@veewo/gitnexus 1.3.8 → 1.3.10
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 +5 -0
- package/dist/benchmark/runner.test.js +1 -1
- package/dist/benchmark/u2-e2e/analyze-parser.d.ts +22 -0
- package/dist/benchmark/u2-e2e/analyze-parser.js +89 -0
- package/dist/benchmark/u2-e2e/analyze-parser.test.js +13 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.d.ts +19 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.js +80 -0
- package/dist/benchmark/u2-e2e/characterlist-assetref.test.js +108 -0
- package/dist/benchmark/u2-e2e/config.d.ts +25 -0
- package/dist/benchmark/u2-e2e/config.js +86 -0
- package/dist/benchmark/u2-e2e/config.test.js +29 -0
- package/dist/benchmark/u2-e2e/metrics.d.ts +20 -0
- package/dist/benchmark/u2-e2e/metrics.js +34 -0
- package/dist/benchmark/u2-e2e/metrics.test.js +13 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.d.ts +33 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.js +439 -0
- package/dist/benchmark/u2-e2e/neonspark-full-e2e.test.js +40 -0
- package/dist/benchmark/u2-e2e/report.d.ts +58 -0
- package/dist/benchmark/u2-e2e/report.js +130 -0
- package/dist/benchmark/u2-e2e/report.test.js +58 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.d.ts +21 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.js +166 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.d.ts +1 -0
- package/dist/benchmark/u2-e2e/retrieval-runner.test.js +145 -0
- package/dist/benchmark/u2-performance-sampler.d.ts +33 -0
- package/dist/benchmark/u2-performance-sampler.js +178 -0
- package/dist/benchmark/u2-performance-sampler.test.d.ts +1 -0
- package/dist/benchmark/u2-performance-sampler.test.js +34 -0
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze-multi-scope-regression.test.js +10 -0
- package/dist/cli/analyze-options.d.ts +19 -0
- package/dist/cli/analyze-options.js +35 -0
- package/dist/cli/analyze-options.test.js +42 -1
- package/dist/cli/analyze-summary.d.ts +7 -0
- package/dist/cli/analyze-summary.js +37 -0
- package/dist/cli/analyze-summary.test.d.ts +1 -0
- package/dist/cli/analyze-summary.test.js +58 -0
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +64 -32
- package/dist/cli/benchmark-u2-e2e.d.ts +9 -0
- package/dist/cli/benchmark-u2-e2e.js +35 -0
- package/dist/cli/benchmark-u2-e2e.test.d.ts +1 -0
- package/dist/cli/benchmark-u2-e2e.test.js +7 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/repo-manager-alias.test.js +24 -1
- package/dist/cli/tool.d.ts +3 -0
- package/dist/cli/tool.js +2 -0
- package/dist/cli/unity-bindings.d.ts +8 -0
- package/dist/cli/unity-bindings.js +33 -0
- package/dist/cli/unity-bindings.test.d.ts +1 -0
- package/dist/cli/unity-bindings.test.js +24 -0
- package/dist/core/graph/types.d.ts +1 -1
- package/dist/core/ingestion/pipeline.d.ts +2 -4
- package/dist/core/ingestion/pipeline.js +12 -0
- package/dist/core/ingestion/unity-resource-processor.d.ts +26 -0
- package/dist/core/ingestion/unity-resource-processor.js +363 -0
- package/dist/core/ingestion/unity-resource-processor.test.d.ts +1 -0
- package/dist/core/ingestion/unity-resource-processor.test.js +599 -0
- package/dist/core/kuzu/kuzu-adapter.d.ts +6 -0
- package/dist/core/kuzu/kuzu-adapter.js +18 -7
- package/dist/core/kuzu/schema.d.ts +2 -2
- package/dist/core/kuzu/schema.js +22 -1
- package/dist/core/kuzu/schema.test.d.ts +1 -0
- package/dist/core/kuzu/schema.test.js +17 -0
- package/dist/core/unity/meta-index.d.ts +5 -0
- package/dist/core/unity/meta-index.js +113 -0
- package/dist/core/unity/meta-index.test.d.ts +1 -0
- package/dist/core/unity/meta-index.test.js +11 -0
- package/dist/core/unity/options.d.ts +2 -0
- package/dist/core/unity/options.js +9 -0
- package/dist/core/unity/options.test.d.ts +1 -0
- package/dist/core/unity/options.test.js +10 -0
- package/dist/core/unity/override-merger.d.ts +27 -0
- package/dist/core/unity/override-merger.js +35 -0
- package/dist/core/unity/override-merger.test.d.ts +1 -0
- package/dist/core/unity/override-merger.test.js +47 -0
- package/dist/core/unity/resolver.d.ts +79 -0
- package/dist/core/unity/resolver.js +384 -0
- package/dist/core/unity/resolver.test.d.ts +1 -0
- package/dist/core/unity/resolver.test.js +244 -0
- package/dist/core/unity/resource-hit-scanner.d.ts +10 -0
- package/dist/core/unity/resource-hit-scanner.js +60 -0
- package/dist/core/unity/resource-hit-scanner.test.d.ts +1 -0
- package/dist/core/unity/resource-hit-scanner.test.js +20 -0
- package/dist/core/unity/scan-context.d.ts +23 -0
- package/dist/core/unity/scan-context.js +318 -0
- package/dist/core/unity/scan-context.test.d.ts +1 -0
- package/dist/core/unity/scan-context.test.js +118 -0
- package/dist/core/unity/serialized-type-index.d.ts +10 -0
- package/dist/core/unity/serialized-type-index.js +105 -0
- package/dist/core/unity/serialized-type-index.test.d.ts +1 -0
- package/dist/core/unity/serialized-type-index.test.js +34 -0
- package/dist/core/unity/u2-thresholds.test.d.ts +1 -0
- package/dist/core/unity/u2-thresholds.test.js +71 -0
- package/dist/core/unity/yaml-object-graph.d.ts +9 -0
- package/dist/core/unity/yaml-object-graph.js +92 -0
- package/dist/core/unity/yaml-object-graph.test.d.ts +1 -0
- package/dist/core/unity/yaml-object-graph.test.js +49 -0
- package/dist/mcp/local/local-backend.js +12 -1
- package/dist/mcp/local/unity-enrichment.d.ts +6 -0
- package/dist/mcp/local/unity-enrichment.js +91 -0
- package/dist/mcp/local/unity-enrichment.test.d.ts +1 -0
- package/dist/mcp/local/unity-enrichment.test.js +130 -0
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/staleness.js +1 -1
- package/dist/mcp/tools.js +12 -0
- package/dist/storage/repo-manager.d.ts +6 -0
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/pipeline.js +2 -0
- package/hooks/check-release-path-hygiene.mjs +108 -0
- package/package.json +16 -9
- package/dist/cli/analyze-custom-modules-regression.test.js +0 -75
- package/dist/cli/analyze-modules-diagnostics.test.js +0 -36
- package/dist/core/ingestion/modules/assignment-engine.d.ts +0 -33
- package/dist/core/ingestion/modules/assignment-engine.js +0 -179
- package/dist/core/ingestion/modules/assignment-engine.test.js +0 -111
- package/dist/core/ingestion/modules/config-loader.d.ts +0 -2
- package/dist/core/ingestion/modules/config-loader.js +0 -186
- package/dist/core/ingestion/modules/config-loader.test.js +0 -57
- package/dist/core/ingestion/modules/rule-matcher.d.ts +0 -12
- package/dist/core/ingestion/modules/rule-matcher.js +0 -63
- package/dist/core/ingestion/modules/rule-matcher.test.js +0 -58
- package/dist/core/ingestion/modules/types.d.ts +0 -44
- package/dist/core/ingestion/modules/types.js +0 -2
- package/dist/mcp/local/cluster-aggregation.d.ts +0 -20
- package/dist/mcp/local/cluster-aggregation.js +0 -48
- package/dist/mcp/local/cluster-aggregation.test.js +0 -22
- /package/dist/{cli/analyze-custom-modules-regression.test.d.ts → benchmark/u2-e2e/analyze-parser.test.d.ts} +0 -0
- /package/dist/{cli/analyze-modules-diagnostics.test.d.ts → benchmark/u2-e2e/characterlist-assetref.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/assignment-engine.test.d.ts → benchmark/u2-e2e/config.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/config-loader.test.d.ts → benchmark/u2-e2e/metrics.test.d.ts} +0 -0
- /package/dist/{core/ingestion/modules/rule-matcher.test.d.ts → benchmark/u2-e2e/neonspark-full-e2e.test.d.ts} +0 -0
- /package/dist/{mcp/local/cluster-aggregation.test.d.ts → benchmark/u2-e2e/report.test.d.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const repoRoot = path.resolve(scriptDir, '..', '..');
|
|
7
|
+
|
|
8
|
+
const FORBIDDEN_ABSOLUTE_PATTERNS = [
|
|
9
|
+
/\/Volumes\/[^"'`\s]*unity-projects\/neonspark/g,
|
|
10
|
+
/\/Users\/[^"'`\s]*unity-projects\/neonspark/g,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const RELEASE_FILES = [
|
|
14
|
+
'gitnexus/package.json',
|
|
15
|
+
'benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const RELEASE_DIR_RULES = [
|
|
19
|
+
{ root: '.github/workflows', exts: new Set(['.yml', '.yaml']) },
|
|
20
|
+
{ root: 'gitnexus/src', exts: new Set(['.ts']) },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function shouldScan(relPath) {
|
|
24
|
+
if (RELEASE_FILES.includes(relPath)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const rule of RELEASE_DIR_RULES) {
|
|
29
|
+
if (!relPath.startsWith(`${rule.root}/`)) continue;
|
|
30
|
+
if (relPath.endsWith('.test.ts')) return false;
|
|
31
|
+
if (rule.exts.has(path.extname(relPath))) return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function walk(dir) {
|
|
38
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
out.push(...(await walk(fullPath)));
|
|
44
|
+
} else if (entry.isFile()) {
|
|
45
|
+
out.push(fullPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function collectReleaseFiles() {
|
|
52
|
+
const roots = ['.github/workflows', 'gitnexus/src'];
|
|
53
|
+
const fullPaths = [];
|
|
54
|
+
for (const root of roots) {
|
|
55
|
+
const abs = path.join(repoRoot, root);
|
|
56
|
+
try {
|
|
57
|
+
fullPaths.push(...(await walk(abs)));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const code = error && typeof error === 'object' ? error.code : undefined;
|
|
60
|
+
if (code !== 'ENOENT') throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const rel of RELEASE_FILES) {
|
|
65
|
+
fullPaths.push(path.join(repoRoot, rel));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dedup = new Map();
|
|
69
|
+
for (const full of fullPaths) {
|
|
70
|
+
const rel = path.relative(repoRoot, full).split(path.sep).join('/');
|
|
71
|
+
if (!shouldScan(rel)) continue;
|
|
72
|
+
dedup.set(rel, full);
|
|
73
|
+
}
|
|
74
|
+
return [...dedup.entries()];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function main() {
|
|
78
|
+
const files = await collectReleaseFiles();
|
|
79
|
+
const hits = [];
|
|
80
|
+
|
|
81
|
+
for (const [relPath, absPath] of files) {
|
|
82
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
83
|
+
for (const pattern of FORBIDDEN_ABSOLUTE_PATTERNS) {
|
|
84
|
+
const matched = content.match(pattern);
|
|
85
|
+
if (matched && matched.length > 0) {
|
|
86
|
+
hits.push({ relPath, matched: matched[0], pattern: String(pattern) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (hits.length > 0) {
|
|
92
|
+
process.stderr.write('Release path hygiene check failed.\n');
|
|
93
|
+
for (const hit of hits) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
`- ${hit.relPath}: contains "${hit.matched}" (pattern: ${hit.pattern})\n`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.stdout.write('Release path hygiene check passed.\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main().catch((error) => {
|
|
106
|
+
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veewo/gitnexus",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -36,18 +36,25 @@
|
|
|
36
36
|
"vendor"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "tsc",
|
|
39
|
+
"build": "rm -rf dist && tsc",
|
|
40
40
|
"dev": "tsx watch src/cli/index.ts",
|
|
41
41
|
"prepare": "npm run build",
|
|
42
|
-
"
|
|
42
|
+
"check:release-paths": "node hooks/check-release-path-hygiene.mjs",
|
|
43
|
+
"check:neonspark-target": "node -e \"const p=(process.env.GITNEXUS_NEONSPARK_TARGET_PATH||'').trim();if(!p){console.error('Missing env: GITNEXUS_NEONSPARK_TARGET_PATH');process.exit(1);}\"",
|
|
44
|
+
"check:u2-e2e-target": "node -e \"const p=(process.env.GITNEXUS_U2_E2E_TARGET_PATH||'').trim();if(!p){console.error('Missing env: GITNEXUS_U2_E2E_TARGET_PATH');process.exit(1);}\"",
|
|
45
|
+
"test:unity": "npm run build && node --test dist/core/unity/*.test.js",
|
|
46
|
+
"test:u3:gates": "npm run check:release-paths && npm run build && node --test dist/benchmark/u2-e2e/*.test.js dist/mcp/local/unity-enrichment.test.js dist/core/ingestion/unity-resource-processor.test.js",
|
|
47
|
+
"test:benchmark": "npm run build && node --test dist/benchmark/*.test.js && node --test dist/cli/*.test.js",
|
|
43
48
|
"benchmark:quick": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/v1 --profile quick --target-path ../benchmarks/fixtures/unity-mini",
|
|
44
49
|
"benchmark:full": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/v1 --profile full --target-path ../benchmarks/fixtures/unity-mini",
|
|
45
|
-
"benchmark:neonspark:full": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v1 --profile full --target-path
|
|
46
|
-
"benchmark:neonspark:quick": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v1 --profile quick --target-path
|
|
47
|
-
"benchmark:neonspark:v2:full": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v2 --profile full --target-path
|
|
48
|
-
"benchmark:neonspark:v2:quick": "npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v2 --profile quick --target-path
|
|
49
|
-
"benchmark:agent-context:quick": "npm run build && node dist/cli/index.js benchmark-agent-context ../benchmarks/agent-context/neonspark-refactor-v1 --profile quick --target-path
|
|
50
|
-
"benchmark:agent-context:full": "npm run build && node dist/cli/index.js benchmark-agent-context ../benchmarks/agent-context/neonspark-refactor-v1 --profile full --target-path
|
|
50
|
+
"benchmark:neonspark:full": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v1 --profile full --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v1/sync-manifest.txt",
|
|
51
|
+
"benchmark:neonspark:quick": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v1 --profile quick --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v1/sync-manifest.txt",
|
|
52
|
+
"benchmark:neonspark:v2:full": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v2 --profile full --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v2/sync-manifest.txt",
|
|
53
|
+
"benchmark:neonspark:v2:quick": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-unity ../benchmarks/unity-baseline/neonspark-v2 --profile quick --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v2/sync-manifest.txt",
|
|
54
|
+
"benchmark:agent-context:quick": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-agent-context ../benchmarks/agent-context/neonspark-refactor-v1 --profile quick --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v2/sync-manifest.txt",
|
|
55
|
+
"benchmark:agent-context:full": "npm run check:neonspark-target && npm run build && node dist/cli/index.js benchmark-agent-context ../benchmarks/agent-context/neonspark-refactor-v1 --profile full --target-path \"$GITNEXUS_NEONSPARK_TARGET_PATH\" --repo-alias neonspark-v1-subset --scope-manifest ../benchmarks/unity-baseline/neonspark-v2/sync-manifest.txt",
|
|
56
|
+
"benchmark:u2:sample": "npm run build && node dist/benchmark/u2-performance-sampler.js",
|
|
57
|
+
"benchmark:u2:e2e": "npm run check:u2-e2e-target && npm run build && node dist/cli/index.js benchmark-u2-e2e --config ../benchmarks/u2-e2e/neonspark-full-u2-e2e.config.json"
|
|
51
58
|
},
|
|
52
59
|
"dependencies": {
|
|
53
60
|
"@huggingface/transformers": "^3.0.0",
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
|
|
7
|
-
const FIXTURE_REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../benchmarks/fixtures/unity-mini');
|
|
8
|
-
async function copyFixtureRepo() {
|
|
9
|
-
const tmpRepo = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-custom-modules-'));
|
|
10
|
-
await fs.cp(FIXTURE_REPO, tmpRepo, { recursive: true });
|
|
11
|
-
await fs.mkdir(path.join(tmpRepo, '.gitnexus'), { recursive: true });
|
|
12
|
-
return tmpRepo;
|
|
13
|
-
}
|
|
14
|
-
test('pipeline mixed mode writes config module + auto fallback memberships', { timeout: 120_000 }, async () => {
|
|
15
|
-
const repoPath = await copyFixtureRepo();
|
|
16
|
-
const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
|
|
17
|
-
await fs.writeFile(configPath, JSON.stringify({
|
|
18
|
-
version: 1,
|
|
19
|
-
mode: 'mixed',
|
|
20
|
-
modules: [
|
|
21
|
-
{
|
|
22
|
-
name: 'Factory',
|
|
23
|
-
defaultPriority: 100,
|
|
24
|
-
rules: [
|
|
25
|
-
{
|
|
26
|
-
id: 'factory-file-rule',
|
|
27
|
-
when: {
|
|
28
|
-
all: [{ field: 'file.path', op: 'contains', value: 'MinionFactory.cs' }],
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
name: 'Battle',
|
|
35
|
-
defaultPriority: 100,
|
|
36
|
-
rules: [],
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
}), 'utf-8');
|
|
40
|
-
const result = await runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] });
|
|
41
|
-
const communities = [...result.graph.iterNodes()].filter((n) => n.label === 'Community');
|
|
42
|
-
const memberships = [...result.graph.iterRelationships()].filter((r) => r.type === 'MEMBER_OF');
|
|
43
|
-
const commById = new Map(communities.map((c) => [c.id, c]));
|
|
44
|
-
const labels = communities.map((c) => String(c.properties.heuristicLabel || c.properties.name || c.id));
|
|
45
|
-
assert.ok(labels.includes('Factory'));
|
|
46
|
-
assert.ok(labels.includes('Battle'));
|
|
47
|
-
const createMembership = memberships.find((m) => m.sourceId.includes('MinionFactory.cs:Create'));
|
|
48
|
-
assert.ok(createMembership);
|
|
49
|
-
const targetCommunity = commById.get(createMembership.targetId);
|
|
50
|
-
assert.ok(targetCommunity);
|
|
51
|
-
assert.equal(String(targetCommunity.properties.heuristicLabel), 'Factory');
|
|
52
|
-
const uniquePerSymbol = new Set(memberships.map((m) => m.sourceId));
|
|
53
|
-
assert.equal(uniquePerSymbol.size, memberships.length);
|
|
54
|
-
const battleCommunity = communities.find((c) => String(c.properties.heuristicLabel) === 'Battle');
|
|
55
|
-
assert.ok(battleCommunity);
|
|
56
|
-
assert.ok(!memberships.some((m) => m.targetId === battleCommunity.id));
|
|
57
|
-
const membershipCommunities = new Set(memberships.map((m) => m.targetId));
|
|
58
|
-
const processCommunities = new Set((result.processResult?.processes || []).flatMap((p) => p.communities || []));
|
|
59
|
-
for (const processCommunity of processCommunities) {
|
|
60
|
-
assert.ok(membershipCommunities.has(processCommunity));
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
test('pipeline mixed + invalid modules.json fails fast', { timeout: 120_000 }, async () => {
|
|
64
|
-
const repoPath = await copyFixtureRepo();
|
|
65
|
-
const configPath = path.join(repoPath, '.gitnexus', 'modules.json');
|
|
66
|
-
await fs.writeFile(configPath, JSON.stringify({
|
|
67
|
-
version: 1,
|
|
68
|
-
mode: 'mixed',
|
|
69
|
-
modules: [
|
|
70
|
-
{ name: 'Dup', defaultPriority: 100, rules: [] },
|
|
71
|
-
{ name: 'Dup', defaultPriority: 100, rules: [] },
|
|
72
|
-
],
|
|
73
|
-
}), 'utf-8');
|
|
74
|
-
await assert.rejects(runPipelineFromRepo(repoPath, () => { }, { includeExtensions: ['.cs'] }), /duplicate module name/i);
|
|
75
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { formatModuleDiagnostics } from './analyze.js';
|
|
7
|
-
test('prints one-time fallback warning for mixed + missing modules.json', () => {
|
|
8
|
-
const lines = formatModuleDiagnostics({
|
|
9
|
-
mode: 'mixed',
|
|
10
|
-
usedFallbackAuto: true,
|
|
11
|
-
warnings: ['modules.json missing in mixed mode, fallback to auto'],
|
|
12
|
-
emptyModules: [],
|
|
13
|
-
configuredModuleCount: 0,
|
|
14
|
-
finalModuleCount: 3,
|
|
15
|
-
});
|
|
16
|
-
assert.ok(lines.some((line) => /fallback to auto/i.test(line)));
|
|
17
|
-
});
|
|
18
|
-
test('prints warning for empty modules', () => {
|
|
19
|
-
const lines = formatModuleDiagnostics({
|
|
20
|
-
mode: 'mixed',
|
|
21
|
-
usedFallbackAuto: false,
|
|
22
|
-
warnings: [],
|
|
23
|
-
emptyModules: ['Battle'],
|
|
24
|
-
configuredModuleCount: 1,
|
|
25
|
-
finalModuleCount: 2,
|
|
26
|
-
});
|
|
27
|
-
assert.ok(lines.some((line) => /empty module/i.test(line)));
|
|
28
|
-
assert.ok(lines.some((line) => /Battle/.test(line)));
|
|
29
|
-
});
|
|
30
|
-
test('README mentions modules.json mode semantics and fallback behavior', async () => {
|
|
31
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const readmePath = path.resolve(here, '../../README.md');
|
|
33
|
-
const readme = await fs.readFile(readmePath, 'utf-8');
|
|
34
|
-
assert.match(readme, /\.gitnexus\/modules\.json/);
|
|
35
|
-
assert.match(readme, /mixed.*fallback.*auto/i);
|
|
36
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { CommunityMembership, CommunityNode } from '../community-processor.js';
|
|
2
|
-
import type { ModuleConfig, ModuleMode } from './types.js';
|
|
3
|
-
import { type MatchableSymbol } from './rule-matcher.js';
|
|
4
|
-
export type AssignmentSource = 'config-rule' | 'auto-fallback';
|
|
5
|
-
export type AssignmentResolvedBy = 'priority' | 'specificity' | 'rule-order' | 'module-lexicographic' | 'fallback-auto';
|
|
6
|
-
export interface AssignmentInput {
|
|
7
|
-
mode: ModuleMode;
|
|
8
|
-
symbols: MatchableSymbol[];
|
|
9
|
-
autoCommunities: CommunityNode[];
|
|
10
|
-
autoMemberships: CommunityMembership[];
|
|
11
|
-
config: ModuleConfig | null;
|
|
12
|
-
}
|
|
13
|
-
export interface SymbolAssignment {
|
|
14
|
-
symbolId: string;
|
|
15
|
-
moduleName: string;
|
|
16
|
-
communityId: string;
|
|
17
|
-
assignmentSource: AssignmentSource;
|
|
18
|
-
matchedRuleId?: string;
|
|
19
|
-
resolvedBy: AssignmentResolvedBy;
|
|
20
|
-
}
|
|
21
|
-
export interface AssignmentDiagnostics {
|
|
22
|
-
mode: ModuleMode;
|
|
23
|
-
configuredModuleCount: number;
|
|
24
|
-
finalModuleCount: number;
|
|
25
|
-
emptyModules: string[];
|
|
26
|
-
}
|
|
27
|
-
export interface AssignmentOutput {
|
|
28
|
-
finalModules: CommunityNode[];
|
|
29
|
-
finalMemberships: CommunityMembership[];
|
|
30
|
-
membershipsBySymbol: Map<string, SymbolAssignment>;
|
|
31
|
-
diagnostics: AssignmentDiagnostics;
|
|
32
|
-
}
|
|
33
|
-
export declare function assignModules(input: AssignmentInput): AssignmentOutput;
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { matchRule, ruleSpecificityScore } from './rule-matcher.js';
|
|
2
|
-
function sanitizeModuleId(name) {
|
|
3
|
-
const base = name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
4
|
-
return base || 'module';
|
|
5
|
-
}
|
|
6
|
-
function resolveBy(top, second) {
|
|
7
|
-
if (!second)
|
|
8
|
-
return 'priority';
|
|
9
|
-
if (top.effectivePriority !== second.effectivePriority)
|
|
10
|
-
return 'priority';
|
|
11
|
-
if (top.specificity !== second.specificity)
|
|
12
|
-
return 'specificity';
|
|
13
|
-
if (top.ruleOrder !== second.ruleOrder)
|
|
14
|
-
return 'rule-order';
|
|
15
|
-
return 'module-lexicographic';
|
|
16
|
-
}
|
|
17
|
-
function pickWinner(candidates) {
|
|
18
|
-
const sorted = [...candidates].sort((a, b) => {
|
|
19
|
-
if (a.effectivePriority !== b.effectivePriority)
|
|
20
|
-
return b.effectivePriority - a.effectivePriority;
|
|
21
|
-
if (a.specificity !== b.specificity)
|
|
22
|
-
return b.specificity - a.specificity;
|
|
23
|
-
if (a.ruleOrder !== b.ruleOrder)
|
|
24
|
-
return a.ruleOrder - b.ruleOrder;
|
|
25
|
-
return a.moduleName.localeCompare(b.moduleName);
|
|
26
|
-
});
|
|
27
|
-
return { winner: sorted[0], resolvedBy: resolveBy(sorted[0], sorted[1]) };
|
|
28
|
-
}
|
|
29
|
-
function buildAutoFallbackMap(autoCommunities) {
|
|
30
|
-
const m = new Map();
|
|
31
|
-
for (const comm of autoCommunities)
|
|
32
|
-
m.set(comm.id, comm);
|
|
33
|
-
return m;
|
|
34
|
-
}
|
|
35
|
-
function toConfiguredCommunities(config) {
|
|
36
|
-
const seen = new Set();
|
|
37
|
-
return config.modules.map((mod) => {
|
|
38
|
-
let idBase = `comm_cfg_${sanitizeModuleId(mod.name)}`;
|
|
39
|
-
let suffix = 1;
|
|
40
|
-
while (seen.has(idBase)) {
|
|
41
|
-
idBase = `comm_cfg_${sanitizeModuleId(mod.name)}_${suffix++}`;
|
|
42
|
-
}
|
|
43
|
-
seen.add(idBase);
|
|
44
|
-
return {
|
|
45
|
-
id: idBase,
|
|
46
|
-
moduleName: mod.name,
|
|
47
|
-
label: mod.name,
|
|
48
|
-
heuristicLabel: mod.name,
|
|
49
|
-
cohesion: 0,
|
|
50
|
-
symbolCount: 0,
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
export function assignModules(input) {
|
|
55
|
-
const membershipsBySymbol = new Map();
|
|
56
|
-
if (input.mode === 'auto' || !input.config) {
|
|
57
|
-
const autoCommunityById = buildAutoFallbackMap(input.autoCommunities);
|
|
58
|
-
for (const membership of input.autoMemberships) {
|
|
59
|
-
const autoComm = autoCommunityById.get(membership.communityId);
|
|
60
|
-
const moduleName = autoComm?.heuristicLabel || autoComm?.label || membership.communityId;
|
|
61
|
-
membershipsBySymbol.set(membership.nodeId, {
|
|
62
|
-
symbolId: membership.nodeId,
|
|
63
|
-
moduleName,
|
|
64
|
-
communityId: membership.communityId,
|
|
65
|
-
assignmentSource: 'auto-fallback',
|
|
66
|
-
resolvedBy: 'fallback-auto',
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
finalModules: input.autoCommunities,
|
|
71
|
-
finalMemberships: input.autoMemberships,
|
|
72
|
-
membershipsBySymbol,
|
|
73
|
-
diagnostics: {
|
|
74
|
-
mode: input.mode,
|
|
75
|
-
configuredModuleCount: 0,
|
|
76
|
-
finalModuleCount: input.autoCommunities.length,
|
|
77
|
-
emptyModules: [],
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const configuredCommunities = toConfiguredCommunities(input.config);
|
|
82
|
-
const configuredCommByName = new Map();
|
|
83
|
-
for (const comm of configuredCommunities) {
|
|
84
|
-
configuredCommByName.set(comm.moduleName, comm);
|
|
85
|
-
}
|
|
86
|
-
const autoCommunityById = buildAutoFallbackMap(input.autoCommunities);
|
|
87
|
-
const symbolById = new Map(input.symbols.map((s) => [s.id, s]));
|
|
88
|
-
const autoMembershipBySymbol = new Map(input.autoMemberships.map((m) => [m.nodeId, m.communityId]));
|
|
89
|
-
const configModuleCounts = new Map();
|
|
90
|
-
const fallbackCommunityCounts = new Map();
|
|
91
|
-
const finalMemberships = [];
|
|
92
|
-
for (const [symbolId, autoCommunityId] of autoMembershipBySymbol.entries()) {
|
|
93
|
-
const symbol = symbolById.get(symbolId);
|
|
94
|
-
if (!symbol)
|
|
95
|
-
continue;
|
|
96
|
-
const candidates = [];
|
|
97
|
-
let ruleOrder = 0;
|
|
98
|
-
for (const moduleDef of input.config.modules) {
|
|
99
|
-
const comm = configuredCommByName.get(moduleDef.name);
|
|
100
|
-
if (!comm)
|
|
101
|
-
continue;
|
|
102
|
-
for (const rule of moduleDef.rules) {
|
|
103
|
-
if (!matchRule(symbol, rule)) {
|
|
104
|
-
ruleOrder += 1;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
candidates.push({
|
|
108
|
-
moduleName: moduleDef.name,
|
|
109
|
-
communityId: comm.id,
|
|
110
|
-
ruleId: rule.id,
|
|
111
|
-
effectivePriority: rule.priority ?? moduleDef.defaultPriority,
|
|
112
|
-
specificity: ruleSpecificityScore(rule),
|
|
113
|
-
ruleOrder,
|
|
114
|
-
});
|
|
115
|
-
ruleOrder += 1;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (candidates.length === 0) {
|
|
119
|
-
finalMemberships.push({ nodeId: symbolId, communityId: autoCommunityId });
|
|
120
|
-
fallbackCommunityCounts.set(autoCommunityId, (fallbackCommunityCounts.get(autoCommunityId) || 0) + 1);
|
|
121
|
-
const autoCommunity = autoCommunityById.get(autoCommunityId);
|
|
122
|
-
membershipsBySymbol.set(symbolId, {
|
|
123
|
-
symbolId,
|
|
124
|
-
moduleName: autoCommunity?.heuristicLabel || autoCommunity?.label || autoCommunityId,
|
|
125
|
-
communityId: autoCommunityId,
|
|
126
|
-
assignmentSource: 'auto-fallback',
|
|
127
|
-
resolvedBy: 'fallback-auto',
|
|
128
|
-
});
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const { winner, resolvedBy } = pickWinner(candidates);
|
|
132
|
-
finalMemberships.push({ nodeId: symbolId, communityId: winner.communityId });
|
|
133
|
-
configModuleCounts.set(winner.moduleName, (configModuleCounts.get(winner.moduleName) || 0) + 1);
|
|
134
|
-
membershipsBySymbol.set(symbolId, {
|
|
135
|
-
symbolId,
|
|
136
|
-
moduleName: winner.moduleName,
|
|
137
|
-
communityId: winner.communityId,
|
|
138
|
-
assignmentSource: 'config-rule',
|
|
139
|
-
matchedRuleId: winner.ruleId,
|
|
140
|
-
resolvedBy,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const emptyModules = [];
|
|
144
|
-
const finalModules = configuredCommunities.map((comm) => {
|
|
145
|
-
const count = configModuleCounts.get(comm.moduleName) || 0;
|
|
146
|
-
if (count === 0)
|
|
147
|
-
emptyModules.push(comm.moduleName);
|
|
148
|
-
return {
|
|
149
|
-
id: comm.id,
|
|
150
|
-
label: comm.label,
|
|
151
|
-
heuristicLabel: comm.heuristicLabel,
|
|
152
|
-
cohesion: 0,
|
|
153
|
-
symbolCount: count,
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
for (const autoComm of input.autoCommunities) {
|
|
157
|
-
const fallbackCount = fallbackCommunityCounts.get(autoComm.id) || 0;
|
|
158
|
-
if (fallbackCount <= 0)
|
|
159
|
-
continue;
|
|
160
|
-
finalModules.push({
|
|
161
|
-
id: autoComm.id,
|
|
162
|
-
label: autoComm.label,
|
|
163
|
-
heuristicLabel: autoComm.heuristicLabel,
|
|
164
|
-
cohesion: autoComm.cohesion,
|
|
165
|
-
symbolCount: fallbackCount,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
finalModules,
|
|
170
|
-
finalMemberships,
|
|
171
|
-
membershipsBySymbol,
|
|
172
|
-
diagnostics: {
|
|
173
|
-
mode: input.mode,
|
|
174
|
-
configuredModuleCount: input.config.modules.length,
|
|
175
|
-
finalModuleCount: finalModules.length,
|
|
176
|
-
emptyModules,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { assignModules } from './assignment-engine.js';
|
|
4
|
-
function baseInput() {
|
|
5
|
-
return {
|
|
6
|
-
mode: 'mixed',
|
|
7
|
-
symbols: [
|
|
8
|
-
{
|
|
9
|
-
id: 'Class:MinionFactory',
|
|
10
|
-
name: 'MinionFactory',
|
|
11
|
-
kind: 'Class',
|
|
12
|
-
filePath: 'Assets/Scripts/MinionFactory.cs',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: 'Class:Minion',
|
|
16
|
-
name: 'Minion',
|
|
17
|
-
kind: 'Class',
|
|
18
|
-
filePath: 'Assets/Scripts/Minion.cs',
|
|
19
|
-
},
|
|
20
|
-
],
|
|
21
|
-
autoCommunities: [
|
|
22
|
-
{
|
|
23
|
-
id: 'comm_0',
|
|
24
|
-
label: 'AutoGroup',
|
|
25
|
-
heuristicLabel: 'AutoGroup',
|
|
26
|
-
cohesion: 0.7,
|
|
27
|
-
symbolCount: 2,
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
autoMemberships: [
|
|
31
|
-
{ nodeId: 'Class:MinionFactory', communityId: 'comm_0' },
|
|
32
|
-
{ nodeId: 'Class:Minion', communityId: 'comm_0' },
|
|
33
|
-
],
|
|
34
|
-
config: {
|
|
35
|
-
version: 1,
|
|
36
|
-
mode: 'mixed',
|
|
37
|
-
modules: [
|
|
38
|
-
{
|
|
39
|
-
name: 'Factory',
|
|
40
|
-
defaultPriority: 100,
|
|
41
|
-
rules: [
|
|
42
|
-
{
|
|
43
|
-
id: 'factory-rule',
|
|
44
|
-
when: {
|
|
45
|
-
all: [{ field: 'symbol.name', op: 'contains', value: 'Factory' }],
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
test('mixed mode applies config override and auto fallback with single membership', () => {
|
|
55
|
-
const out = assignModules(baseInput());
|
|
56
|
-
assert.equal(out.finalMemberships.length, 2);
|
|
57
|
-
assert.equal(new Set(out.finalMemberships.map((m) => m.nodeId)).size, 2);
|
|
58
|
-
assert.equal(out.membershipsBySymbol.get('Class:MinionFactory')?.moduleName, 'Factory');
|
|
59
|
-
assert.equal(out.membershipsBySymbol.get('Class:MinionFactory')?.assignmentSource, 'config-rule');
|
|
60
|
-
assert.equal(out.membershipsBySymbol.get('Class:Minion')?.assignmentSource, 'auto-fallback');
|
|
61
|
-
});
|
|
62
|
-
test('conflict resolution order: priority > specificity > rule-order > module-name', () => {
|
|
63
|
-
const input = baseInput();
|
|
64
|
-
input.symbols = [{ id: 'Class:X', name: 'X', kind: 'Class', filePath: 'x.ts' }];
|
|
65
|
-
input.autoMemberships = [{ nodeId: 'Class:X', communityId: 'comm_0' }];
|
|
66
|
-
input.autoCommunities = [{ id: 'comm_0', label: 'Auto', heuristicLabel: 'Auto', cohesion: 0.4, symbolCount: 1 }];
|
|
67
|
-
input.config = {
|
|
68
|
-
version: 1,
|
|
69
|
-
mode: 'mixed',
|
|
70
|
-
modules: [
|
|
71
|
-
{
|
|
72
|
-
name: 'ContainsWinsByOrder',
|
|
73
|
-
defaultPriority: 100,
|
|
74
|
-
rules: [{ id: 'contains-a', when: { all: [{ field: 'symbol.name', op: 'contains', value: 'X' }] } }],
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'EqWinsBySpecificity',
|
|
78
|
-
defaultPriority: 100,
|
|
79
|
-
rules: [{ id: 'eq-b', when: { all: [{ field: 'symbol.name', op: 'eq', value: 'X' }] } }],
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
};
|
|
83
|
-
const out = assignModules(input);
|
|
84
|
-
assert.equal(out.membershipsBySymbol.get('Class:X')?.moduleName, 'EqWinsBySpecificity');
|
|
85
|
-
assert.equal(out.membershipsBySymbol.get('Class:X')?.resolvedBy, 'specificity');
|
|
86
|
-
});
|
|
87
|
-
test('higher effective priority wins even with lower specificity', () => {
|
|
88
|
-
const input = baseInput();
|
|
89
|
-
input.symbols = [{ id: 'Class:X', name: 'X', kind: 'Class', filePath: 'x.ts' }];
|
|
90
|
-
input.autoMemberships = [{ nodeId: 'Class:X', communityId: 'comm_0' }];
|
|
91
|
-
input.autoCommunities = [{ id: 'comm_0', label: 'Auto', heuristicLabel: 'Auto', cohesion: 0.4, symbolCount: 1 }];
|
|
92
|
-
input.config = {
|
|
93
|
-
version: 1,
|
|
94
|
-
mode: 'mixed',
|
|
95
|
-
modules: [
|
|
96
|
-
{
|
|
97
|
-
name: 'HighPriorityContains',
|
|
98
|
-
defaultPriority: 120,
|
|
99
|
-
rules: [{ id: 'contains-a', when: { all: [{ field: 'symbol.name', op: 'contains', value: 'X' }] } }],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'LowPriorityEq',
|
|
103
|
-
defaultPriority: 100,
|
|
104
|
-
rules: [{ id: 'eq-b', when: { all: [{ field: 'symbol.name', op: 'eq', value: 'X' }] } }],
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
};
|
|
108
|
-
const out = assignModules(input);
|
|
109
|
-
assert.equal(out.membershipsBySymbol.get('Class:X')?.moduleName, 'HighPriorityContains');
|
|
110
|
-
assert.equal(out.membershipsBySymbol.get('Class:X')?.resolvedBy, 'priority');
|
|
111
|
-
});
|