@veewo/gitnexus 1.5.0-rc.4 → 1.5.0
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/dist/benchmark/analyze-runner.d.ts +1 -1
- package/dist/benchmark/analyze-runner.js +4 -3
- package/dist/benchmark/analyze-runner.test.js +7 -0
- package/dist/cli/ai-context.d.ts +0 -1
- package/dist/cli/ai-context.js +15 -6
- package/dist/cli/analyze-options.js +58 -34
- package/dist/cli/analyze-options.test.js +57 -0
- package/dist/cli/analyze-runtime-summary.js +1 -0
- package/dist/cli/analyze-runtime-summary.test.js +10 -0
- package/dist/cli/analyze-summary.d.ts +2 -0
- package/dist/cli/analyze-summary.js +19 -0
- package/dist/cli/analyze.d.ts +11 -0
- package/dist/cli/analyze.js +30 -5
- package/dist/cli/analyze.test.d.ts +1 -0
- package/dist/cli/analyze.test.js +25 -0
- package/dist/cli/benchmark-agent-context.js +1 -1
- package/dist/cli/benchmark-unity.js +1 -1
- package/dist/cli/benchmark-unity.test.js +5 -1
- package/dist/cli/index.js +4 -2
- package/dist/cli/scope-manifest-config.d.ts +9 -0
- package/dist/cli/scope-manifest-config.js +37 -0
- package/dist/cli/setup.js +40 -41
- package/dist/cli/setup.test.js +14 -14
- package/dist/cli/sync-manifest.d.ts +27 -0
- package/dist/cli/sync-manifest.js +200 -0
- package/dist/cli/sync-manifest.test.d.ts +1 -0
- package/dist/cli/sync-manifest.test.js +88 -0
- package/dist/core/config/unity-config.d.ts +1 -0
- package/dist/core/config/unity-config.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +2 -1
- package/dist/core/ingestion/call-processor.js +28 -6
- package/dist/core/ingestion/heritage-processor.d.ts +2 -1
- package/dist/core/ingestion/heritage-processor.js +30 -7
- package/dist/core/ingestion/import-processor.d.ts +2 -1
- package/dist/core/ingestion/import-processor.js +28 -6
- package/dist/core/ingestion/parsing-processor.d.ts +5 -3
- package/dist/core/ingestion/parsing-processor.js +46 -13
- package/dist/core/ingestion/pipeline.js +65 -13
- package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +1 -1
- package/dist/core/ingestion/unity-runtime-binding-rules.js +21 -18
- package/dist/core/ingestion/workers/parse-worker.d.ts +2 -0
- package/dist/core/ingestion/workers/parse-worker.js +50 -6
- package/dist/core/tree-sitter/csharp-define-profile.d.ts +6 -0
- package/dist/core/tree-sitter/csharp-define-profile.js +43 -0
- package/dist/core/tree-sitter/csharp-preproc-normalizer.d.ts +14 -0
- package/dist/core/tree-sitter/csharp-preproc-normalizer.js +261 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +10 -0
- package/dist/core/tree-sitter/parser-loader.js +19 -0
- package/dist/types/pipeline.d.ts +13 -0
- package/package.json +12 -12
- package/scripts/check-sync-manifest-traceability.mjs +203 -0
- package/scripts/tree-sitter-audit-classify.mjs +172 -0
- package/skills/gitnexus-cli.md +36 -4
- package/skills/gitnexus-unity-rule-gen.md +2 -2
|
@@ -13,10 +13,11 @@ export function buildAnalyzeArgs(repoPath, options) {
|
|
|
13
13
|
'dist/cli/index.js',
|
|
14
14
|
'analyze',
|
|
15
15
|
'--force',
|
|
16
|
-
'--extensions',
|
|
17
|
-
options.extensions,
|
|
18
|
-
repoPath,
|
|
19
16
|
];
|
|
17
|
+
if (options.extensions !== undefined) {
|
|
18
|
+
args.push('--extensions', options.extensions);
|
|
19
|
+
}
|
|
20
|
+
args.push(repoPath);
|
|
20
21
|
if (options.repoAlias) {
|
|
21
22
|
args.push('--repo-alias', options.repoAlias);
|
|
22
23
|
}
|
|
@@ -35,3 +35,10 @@ test('buildAnalyzeArgs forwards alias and scope options', () => {
|
|
|
35
35
|
'Packages/com.veewo.*',
|
|
36
36
|
]);
|
|
37
37
|
});
|
|
38
|
+
test('buildAnalyzeArgs omits --extensions when not explicitly provided', () => {
|
|
39
|
+
const args = buildAnalyzeArgs('/repo/path', {
|
|
40
|
+
repoAlias: 'neonspark-v1-subset',
|
|
41
|
+
scopeManifest: '/tmp/scope-manifest.txt',
|
|
42
|
+
});
|
|
43
|
+
assert.equal(args.includes('--extensions'), false);
|
|
44
|
+
});
|
package/dist/cli/ai-context.d.ts
CHANGED
|
@@ -20,7 +20,6 @@ interface RepoStats {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function generateAIContextFiles(repoPath: string, _storagePath: string, projectName: string, stats: RepoStats, options?: {
|
|
22
22
|
skillScope?: SkillScope;
|
|
23
|
-
cliPackageSpec?: string;
|
|
24
23
|
}, generatedSkills?: GeneratedSkillInfo[]): Promise<{
|
|
25
24
|
files: string[];
|
|
26
25
|
}>;
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { buildNpxCommand, resolveCliSpec } from '../config/cli-spec.js';
|
|
12
11
|
// ESM equivalent of __dirname
|
|
13
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -25,14 +24,13 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
|
25
24
|
* - Exact tool commands with parameters — vague directives get ignored
|
|
26
25
|
* - Self-review checklist — forces model to verify its own work
|
|
27
26
|
*/
|
|
28
|
-
function generateGitNexusContent(projectName, stats, skillScope,
|
|
27
|
+
function generateGitNexusContent(projectName, stats, skillScope, generatedSkills) {
|
|
29
28
|
const skillRoot = skillScope === 'global'
|
|
30
29
|
? '~/.agents/skills/gitnexus'
|
|
31
30
|
: '.agents/skills/gitnexus';
|
|
32
31
|
const generatedRows = (generatedSkills && generatedSkills.length > 0)
|
|
33
32
|
? `\n${generatedSkills.map((s) => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`).join('\n')}`
|
|
34
33
|
: '';
|
|
35
|
-
const reindexCmd = buildNpxCommand(cliPackageSpec, 'analyze');
|
|
36
34
|
return `${GITNEXUS_START_MARKER}
|
|
37
35
|
# GitNexus MCP
|
|
38
36
|
|
|
@@ -44,7 +42,7 @@ function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec,
|
|
|
44
42
|
4. **Follow config/state file rules:** \`docs/gitnexus-config-files.md\`
|
|
45
43
|
5. **If user asks to release/publish a specific version and this repo has \`DISTRIBUTION.md\`, execute that workflow in full-release mode by default** (unless user explicitly asks \`prepare-only\` or \`publish-only\`).
|
|
46
44
|
|
|
47
|
-
> If step 1 warns the index is stale, ask user whether to rebuild index via \`gitnexus analyze\` when local CLI exists; otherwise resolve the pinned npx package spec from \`~/.gitnexus/config.json\` (\`cliPackageSpec\` first, then \`cliVersion\`) and run
|
|
45
|
+
> If step 1 warns the index is stale, ask user whether to rebuild index via \`gitnexus analyze\` when local CLI exists; otherwise resolve the pinned npx package spec from \`~/.gitnexus/config.json\` (\`cliPackageSpec\` first, then \`cliVersion\`) and run \`npx -y <resolved-spec> analyze\` (it reuses previous analyze scope/options by default; add \`--no-reuse-options\` to reset). If user declines, explicitly warn that retrieval may not reflect current codebase. For build/analyze/test commands, use a 10-30 minute timeout; on failure/timeout, report exact tool output and do not auto-retry or silently fall back to glob/grep.
|
|
48
46
|
|
|
49
47
|
## Skills
|
|
50
48
|
|
|
@@ -58,6 +56,18 @@ function generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec,
|
|
|
58
56
|
| Index, status, clean, wiki CLI commands | \`${skillRoot}/gitnexus-cli/SKILL.md\` |
|
|
59
57
|
| Create Unity analyze_rules interactively | \`${skillRoot}/gitnexus-unity-rule-gen/SKILL.md\` |${generatedRows}
|
|
60
58
|
|
|
59
|
+
## Dev Workflow (Source Build)
|
|
60
|
+
|
|
61
|
+
To use a locally built dist instead of the globally installed package (useful when testing unreleased changes):
|
|
62
|
+
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
cd /path/to/GitNexus/gitnexus
|
|
65
|
+
npm run build
|
|
66
|
+
npm link # replaces global install with symlink to local dist/cli/index.js
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
After \`npm link\`, \`gitnexus\` on this machine points to the local dist. All repos using \`gitnexus mcp\` in their MCP config will pick up the new build after restarting the agent session. To restore the published package: \`npm unlink -g @veewo/gitnexus && npm install -g @veewo/gitnexus\`.
|
|
70
|
+
|
|
61
71
|
${GITNEXUS_END_MARKER}`;
|
|
62
72
|
}
|
|
63
73
|
/**
|
|
@@ -204,8 +214,7 @@ async function copyDirRecursive(src, dest) {
|
|
|
204
214
|
*/
|
|
205
215
|
export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, options, generatedSkills) {
|
|
206
216
|
const skillScope = options?.skillScope === 'global' ? 'global' : 'project';
|
|
207
|
-
const
|
|
208
|
-
const content = generateGitNexusContent(projectName, stats, skillScope, cliPackageSpec, generatedSkills);
|
|
217
|
+
const content = generateGitNexusContent(projectName, stats, skillScope, generatedSkills);
|
|
209
218
|
const createdFiles = [];
|
|
210
219
|
// Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Codex, Cline, etc.)
|
|
211
220
|
const agentsPath = path.join(repoPath, 'AGENTS.md');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { normalizeScopeRules
|
|
3
|
+
import { normalizeScopeRules } from '../core/ingestion/scope-filter.js';
|
|
4
|
+
import { parseScopeManifestConfig } from './scope-manifest-config.js';
|
|
4
5
|
const REPO_ALIAS_REGEX = /^[a-zA-Z0-9._-]{3,64}$/;
|
|
5
6
|
export function parseExtensionList(rawExtensions) {
|
|
6
7
|
return (rawExtensions || '')
|
|
@@ -21,38 +22,16 @@ export function normalizeRepoAlias(repoAlias) {
|
|
|
21
22
|
return normalized;
|
|
22
23
|
}
|
|
23
24
|
export async function resolveAnalyzeScopeRules(options) {
|
|
24
|
-
|
|
25
|
+
let manifestRules = [];
|
|
25
26
|
if (options?.scopeManifest) {
|
|
26
27
|
const manifestPath = path.resolve(options.scopeManifest);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
content = await fs.readFile(manifestPath, 'utf-8');
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new Error(`Scope manifest not found: ${manifestPath}`);
|
|
33
|
-
}
|
|
34
|
-
const manifestRules = parseScopeRules(content);
|
|
28
|
+
const manifest = await readScopeManifestConfig(manifestPath);
|
|
29
|
+
manifestRules = manifest.scopeRules;
|
|
35
30
|
if (manifestRules.length === 0) {
|
|
36
31
|
throw new Error(`Scope manifest has no valid scope rules: ${manifestPath}`);
|
|
37
32
|
}
|
|
38
|
-
rules.push(...manifestRules);
|
|
39
|
-
}
|
|
40
|
-
const prefixesRaw = Array.isArray(options?.scopePrefix)
|
|
41
|
-
? options?.scopePrefix || []
|
|
42
|
-
: options?.scopePrefix
|
|
43
|
-
? [options.scopePrefix]
|
|
44
|
-
: [];
|
|
45
|
-
for (const prefix of prefixesRaw) {
|
|
46
|
-
const trimmed = prefix.trim();
|
|
47
|
-
if (trimmed) {
|
|
48
|
-
rules.push(trimmed);
|
|
49
|
-
}
|
|
50
33
|
}
|
|
51
|
-
|
|
52
|
-
if ((options?.scopeManifest || prefixesRaw.length > 0) && normalizedRules.length === 0) {
|
|
53
|
-
throw new Error('No valid scope rules provided.');
|
|
54
|
-
}
|
|
55
|
-
return normalizedRules;
|
|
34
|
+
return resolveScopeRulesFromInput(manifestRules, normalizeScopePrefixes(options?.scopePrefix), Boolean(options?.scopeManifest));
|
|
56
35
|
}
|
|
57
36
|
function parseScopePrefixCount(scopePrefix) {
|
|
58
37
|
if (Array.isArray(scopePrefix))
|
|
@@ -62,26 +41,36 @@ function parseScopePrefixCount(scopePrefix) {
|
|
|
62
41
|
return 0;
|
|
63
42
|
}
|
|
64
43
|
export async function resolveEffectiveAnalyzeOptions(options, stored) {
|
|
44
|
+
const manifestConfig = options?.scopeManifest
|
|
45
|
+
? await readScopeManifestConfig(path.resolve(options.scopeManifest))
|
|
46
|
+
: undefined;
|
|
65
47
|
const includeExtensionsFromCli = parseExtensionList(options?.extensions);
|
|
66
|
-
const scopeRulesFromCli =
|
|
67
|
-
scopeManifest: options?.scopeManifest,
|
|
68
|
-
scopePrefix: options?.scopePrefix,
|
|
69
|
-
});
|
|
48
|
+
const scopeRulesFromCli = resolveScopeRulesFromInput(manifestConfig?.scopeRules || [], normalizeScopePrefixes(options?.scopePrefix), Boolean(options?.scopeManifest));
|
|
70
49
|
const repoAliasFromCli = normalizeRepoAlias(options?.repoAlias);
|
|
50
|
+
const manifestExtensions = manifestConfig?.directives.extensions;
|
|
51
|
+
const manifestRepoAlias = manifestConfig?.directives.repoAlias;
|
|
52
|
+
const manifestEmbeddings = manifestConfig?.directives.embeddings;
|
|
71
53
|
const hasCliExtensions = options?.extensions !== undefined;
|
|
72
54
|
const hasCliScope = Boolean(options?.scopeManifest) || parseScopePrefixCount(options?.scopePrefix) > 0;
|
|
73
55
|
const hasCliRepoAlias = options?.repoAlias !== undefined;
|
|
74
56
|
const canReuse = options?.reuseOptions !== false;
|
|
75
57
|
const includeExtensions = hasCliExtensions
|
|
76
58
|
? includeExtensionsFromCli
|
|
77
|
-
: (
|
|
59
|
+
: (manifestExtensions !== undefined
|
|
60
|
+
? parseExtensionList(manifestExtensions)
|
|
61
|
+
: (canReuse ? (stored?.includeExtensions || []) : []));
|
|
78
62
|
const scopeRules = hasCliScope
|
|
79
63
|
? scopeRulesFromCli
|
|
80
64
|
: (canReuse ? (stored?.scopeRules || []) : []);
|
|
81
65
|
const repoAlias = hasCliRepoAlias
|
|
82
66
|
? repoAliasFromCli
|
|
83
|
-
: (
|
|
84
|
-
|
|
67
|
+
: (manifestRepoAlias !== undefined
|
|
68
|
+
? normalizeRepoAlias(manifestRepoAlias)
|
|
69
|
+
: (canReuse ? normalizeRepoAlias(stored?.repoAlias) : undefined));
|
|
70
|
+
const embeddings = options?.embeddings
|
|
71
|
+
?? (manifestEmbeddings !== undefined
|
|
72
|
+
? parseManifestEmbeddings(manifestEmbeddings)
|
|
73
|
+
: (canReuse ? Boolean(stored?.embeddings) : false));
|
|
85
74
|
return {
|
|
86
75
|
includeExtensions: [...includeExtensions],
|
|
87
76
|
scopeRules: [...scopeRules],
|
|
@@ -89,3 +78,38 @@ export async function resolveEffectiveAnalyzeOptions(options, stored) {
|
|
|
89
78
|
embeddings,
|
|
90
79
|
};
|
|
91
80
|
}
|
|
81
|
+
function normalizeScopePrefixes(scopePrefix) {
|
|
82
|
+
const prefixesRaw = Array.isArray(scopePrefix)
|
|
83
|
+
? scopePrefix || []
|
|
84
|
+
: scopePrefix
|
|
85
|
+
? [scopePrefix]
|
|
86
|
+
: [];
|
|
87
|
+
return prefixesRaw
|
|
88
|
+
.map((prefix) => prefix.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
function resolveScopeRulesFromInput(manifestRules, prefixes, hasScopeManifest) {
|
|
92
|
+
const normalizedRules = normalizeScopeRules([...manifestRules, ...prefixes]);
|
|
93
|
+
if ((hasScopeManifest || prefixes.length > 0) && normalizedRules.length === 0) {
|
|
94
|
+
throw new Error('No valid scope rules provided.');
|
|
95
|
+
}
|
|
96
|
+
return normalizedRules;
|
|
97
|
+
}
|
|
98
|
+
async function readScopeManifestConfig(manifestPath) {
|
|
99
|
+
let content;
|
|
100
|
+
try {
|
|
101
|
+
content = await fs.readFile(manifestPath, 'utf-8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
throw new Error(`Scope manifest not found: ${manifestPath}`);
|
|
105
|
+
}
|
|
106
|
+
return parseScopeManifestConfig(content);
|
|
107
|
+
}
|
|
108
|
+
function parseManifestEmbeddings(raw) {
|
|
109
|
+
const normalized = raw.trim().toLowerCase();
|
|
110
|
+
if (normalized === 'true')
|
|
111
|
+
return true;
|
|
112
|
+
if (normalized === 'false')
|
|
113
|
+
return false;
|
|
114
|
+
throw new Error(`Invalid @embeddings directive value: ${raw}. Expected true or false.`);
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { normalizeRepoAlias, parseExtensionList, resolveAnalyzeScopeRules, resolveEffectiveAnalyzeOptions, } from './analyze-options.js';
|
|
7
|
+
import { parseScopeManifestConfig } from './scope-manifest-config.js';
|
|
7
8
|
test('parseExtensionList normalizes dot prefixes', () => {
|
|
8
9
|
const exts = parseExtensionList('cs,.ts, go ');
|
|
9
10
|
assert.deepEqual(exts, ['.cs', '.ts', '.go']);
|
|
@@ -75,3 +76,59 @@ test('resolveEffectiveAnalyzeOptions prefers explicit CLI values over stored set
|
|
|
75
76
|
assert.equal(resolved.repoAlias, 'new-alias');
|
|
76
77
|
assert.equal(resolved.embeddings, false);
|
|
77
78
|
});
|
|
79
|
+
test('resolveEffectiveAnalyzeOptions reads @extensions/@repoAlias/@embeddings from manifest', async () => {
|
|
80
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-manifest-directives-'));
|
|
81
|
+
const manifestPath = path.join(tmpDir, 'sync-manifest.txt');
|
|
82
|
+
await fs.writeFile(manifestPath, ['Assets/', '@extensions=.cs,.meta', '@repoAlias=neonspark-core', '@embeddings=false'].join('\n'), 'utf-8');
|
|
83
|
+
const resolved = await resolveEffectiveAnalyzeOptions({ scopeManifest: manifestPath }, {
|
|
84
|
+
includeExtensions: ['.ts'],
|
|
85
|
+
scopeRules: ['src'],
|
|
86
|
+
repoAlias: 'stored-alias',
|
|
87
|
+
embeddings: true,
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(resolved.scopeRules, ['Assets']);
|
|
90
|
+
assert.deepEqual(resolved.includeExtensions, ['.cs', '.meta']);
|
|
91
|
+
assert.equal(resolved.repoAlias, 'neonspark-core');
|
|
92
|
+
assert.equal(resolved.embeddings, false);
|
|
93
|
+
});
|
|
94
|
+
test('resolveEffectiveAnalyzeOptions enforces precedence CLI > manifest > meta', async () => {
|
|
95
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-manifest-precedence-'));
|
|
96
|
+
const manifestPath = path.join(tmpDir, 'sync-manifest.txt');
|
|
97
|
+
await fs.writeFile(manifestPath, ['Assets/', '@extensions=.cs,.meta', '@repoAlias=manifest-alias', '@embeddings=false'].join('\n'), 'utf-8');
|
|
98
|
+
const resolved = await resolveEffectiveAnalyzeOptions({
|
|
99
|
+
scopeManifest: manifestPath,
|
|
100
|
+
extensions: '.ts',
|
|
101
|
+
}, {
|
|
102
|
+
includeExtensions: ['.js'],
|
|
103
|
+
scopeRules: ['tools'],
|
|
104
|
+
repoAlias: 'meta-alias',
|
|
105
|
+
embeddings: true,
|
|
106
|
+
});
|
|
107
|
+
assert.deepEqual(resolved.scopeRules, ['Assets']);
|
|
108
|
+
assert.deepEqual(resolved.includeExtensions, ['.ts']);
|
|
109
|
+
assert.equal(resolved.repoAlias, 'manifest-alias');
|
|
110
|
+
assert.equal(resolved.embeddings, false);
|
|
111
|
+
});
|
|
112
|
+
test('resolveEffectiveAnalyzeOptions rejects unknown manifest directives', async () => {
|
|
113
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-manifest-unknown-'));
|
|
114
|
+
const manifestPath = path.join(tmpDir, 'sync-manifest.txt');
|
|
115
|
+
await fs.writeFile(manifestPath, ['Assets/', '@foo=bar'].join('\n'), 'utf-8');
|
|
116
|
+
await assert.rejects(resolveEffectiveAnalyzeOptions({ scopeManifest: manifestPath }), /unknown manifest directive/i);
|
|
117
|
+
});
|
|
118
|
+
test('parseScopeManifestConfig splits scope rules and directives', () => {
|
|
119
|
+
const parsed = parseScopeManifestConfig([
|
|
120
|
+
'# comment',
|
|
121
|
+
'Assets/',
|
|
122
|
+
'Packages/com.veewo.*',
|
|
123
|
+
'',
|
|
124
|
+
'@extensions=.cs,.meta',
|
|
125
|
+
'@repoAlias=demo-repo',
|
|
126
|
+
'@embeddings=false',
|
|
127
|
+
].join('\n'));
|
|
128
|
+
assert.deepEqual(parsed.scopeRules, ['Assets', 'Packages/com.veewo.*']);
|
|
129
|
+
assert.deepEqual(parsed.directives, {
|
|
130
|
+
extensions: '.cs,.meta',
|
|
131
|
+
repoAlias: 'demo-repo',
|
|
132
|
+
embeddings: 'false',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -7,8 +7,18 @@ test('toPipelineRuntimeSummary drops graph reference and preserves reporting fie
|
|
|
7
7
|
communityResult: { stats: { totalCommunities: 3 } },
|
|
8
8
|
processResult: { stats: { totalProcesses: 2 } },
|
|
9
9
|
unityResult: { diagnostics: ['scanContext: scripts=1'] },
|
|
10
|
+
csharpPreprocDiagnostics: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
defineSymbolCount: 2,
|
|
13
|
+
normalizedFiles: 1,
|
|
14
|
+
fallbackFiles: 0,
|
|
15
|
+
skippedFiles: 3,
|
|
16
|
+
expressionErrors: 0,
|
|
17
|
+
undefinedSymbols: [],
|
|
18
|
+
},
|
|
10
19
|
});
|
|
11
20
|
assert.equal('graph' in out, false);
|
|
12
21
|
assert.equal(out.totalFileCount, 12);
|
|
13
22
|
assert.equal(out.communityResult?.stats.totalCommunities, 3);
|
|
23
|
+
assert.equal(out.csharpPreprocDiagnostics?.normalizedFiles, 1);
|
|
14
24
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { CSharpPreprocDiagnostics } from '../types/pipeline.js';
|
|
1
2
|
export interface FallbackInsertStats {
|
|
2
3
|
attempted: number;
|
|
3
4
|
succeeded: number;
|
|
4
5
|
failed: number;
|
|
5
6
|
}
|
|
7
|
+
export declare function formatCSharpPreprocDiagnosticsSummary(diagnostics: CSharpPreprocDiagnostics | undefined, previewLimit?: number): string[];
|
|
6
8
|
export declare function formatUnityDiagnosticsSummary(diagnostics: string[] | undefined, previewLimit?: number): string[];
|
|
7
9
|
export declare function formatFallbackSummary(warnings: string[] | undefined, stats: FallbackInsertStats | undefined, previewLimit?: number): string[];
|
|
8
10
|
export declare function resolveFallbackStats(warnings: string[] | undefined, stats: FallbackInsertStats | undefined): FallbackInsertStats;
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
export function formatCSharpPreprocDiagnosticsSummary(diagnostics, previewLimit = 5) {
|
|
2
|
+
if (!diagnostics?.enabled)
|
|
3
|
+
return [];
|
|
4
|
+
const lines = [
|
|
5
|
+
`CSharp Preproc: defines=${diagnostics.defineSymbolCount}, normalized=${diagnostics.normalizedFiles}, fallback=${diagnostics.fallbackFiles}, skipped=${diagnostics.skippedFiles}, exprErrors=${diagnostics.expressionErrors}`,
|
|
6
|
+
];
|
|
7
|
+
if (diagnostics.sourcePath) {
|
|
8
|
+
lines.push(`- source: ${diagnostics.sourcePath}`);
|
|
9
|
+
}
|
|
10
|
+
if (diagnostics.undefinedSymbols.length > 0) {
|
|
11
|
+
const limit = previewLimit > 0 ? previewLimit : diagnostics.undefinedSymbols.length;
|
|
12
|
+
const preview = diagnostics.undefinedSymbols.slice(0, limit);
|
|
13
|
+
lines.push(`- undefined symbols: ${preview.join(', ')}`);
|
|
14
|
+
if (diagnostics.undefinedSymbols.length > preview.length) {
|
|
15
|
+
lines.push(`... ${diagnostics.undefinedSymbols.length - preview.length} more`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return lines;
|
|
19
|
+
}
|
|
1
20
|
export function formatUnityDiagnosticsSummary(diagnostics, previewLimit = 3) {
|
|
2
21
|
if (!diagnostics || diagnostics.length === 0) {
|
|
3
22
|
return [];
|
package/dist/cli/analyze.d.ts
CHANGED
|
@@ -3,15 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Indexes a repository and stores the knowledge graph in .gitnexus/
|
|
5
5
|
*/
|
|
6
|
+
import { type SyncManifestPolicy } from './sync-manifest.js';
|
|
6
7
|
export interface AnalyzeOptions {
|
|
7
8
|
force?: boolean;
|
|
8
9
|
embeddings?: boolean;
|
|
9
10
|
extensions?: string;
|
|
10
11
|
repoAlias?: string;
|
|
12
|
+
csharpDefineCsproj?: string;
|
|
11
13
|
scopeManifest?: string;
|
|
12
14
|
scopePrefix?: string[];
|
|
15
|
+
syncManifestPolicy?: SyncManifestPolicy;
|
|
13
16
|
reuseOptions?: boolean;
|
|
14
17
|
skills?: boolean;
|
|
15
18
|
verbose?: boolean;
|
|
16
19
|
}
|
|
17
20
|
export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
|
|
21
|
+
export declare function buildPipelineRunOptionsForAnalyze(resolvedOptions: {
|
|
22
|
+
includeExtensions: string[];
|
|
23
|
+
scopeRules: string[];
|
|
24
|
+
}, options?: AnalyzeOptions): {
|
|
25
|
+
includeExtensions: string[];
|
|
26
|
+
scopeRules: string[];
|
|
27
|
+
csharpDefineCsproj?: string;
|
|
28
|
+
};
|
package/dist/cli/analyze.js
CHANGED
|
@@ -19,10 +19,10 @@ import { generateAIContextFiles } from './ai-context.js';
|
|
|
19
19
|
import { generateSkillFiles } from './skill-gen.js';
|
|
20
20
|
import fs from 'fs/promises';
|
|
21
21
|
import { resolveEffectiveAnalyzeOptions } from './analyze-options.js';
|
|
22
|
-
import { formatFallbackSummary, formatUnityDiagnosticsSummary, resolveFallbackStats } from './analyze-summary.js';
|
|
22
|
+
import { formatCSharpPreprocDiagnosticsSummary, formatFallbackSummary, formatUnityDiagnosticsSummary, resolveFallbackStats } from './analyze-summary.js';
|
|
23
23
|
import { resolveChildProcessExit } from './exit-code.js';
|
|
24
24
|
import { toPipelineRuntimeSummary } from './analyze-runtime-summary.js';
|
|
25
|
-
import {
|
|
25
|
+
import { enforceSyncManifestConsistency, resolveScopeManifestForAnalyze } from './sync-manifest.js';
|
|
26
26
|
const HEAP_MB = 8192;
|
|
27
27
|
const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
|
|
28
28
|
/** Re-exec the process with an 8GB heap if we're currently below that. */
|
|
@@ -112,9 +112,21 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
112
112
|
let repoAlias;
|
|
113
113
|
let embeddingsEnabled = false;
|
|
114
114
|
try {
|
|
115
|
+
const scopeManifest = await resolveScopeManifestForAnalyze(repoPath, {
|
|
116
|
+
scopeManifest: options?.scopeManifest,
|
|
117
|
+
scopePrefix: options?.scopePrefix,
|
|
118
|
+
});
|
|
119
|
+
await enforceSyncManifestConsistency({
|
|
120
|
+
manifestPath: scopeManifest,
|
|
121
|
+
extensions: options?.extensions,
|
|
122
|
+
repoAlias: options?.repoAlias,
|
|
123
|
+
embeddings: options?.embeddings,
|
|
124
|
+
policy: options?.syncManifestPolicy,
|
|
125
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
126
|
+
});
|
|
115
127
|
const effectiveOptions = await resolveEffectiveAnalyzeOptions({
|
|
116
128
|
extensions: options?.extensions,
|
|
117
|
-
scopeManifest
|
|
129
|
+
scopeManifest,
|
|
118
130
|
scopePrefix: options?.scopePrefix,
|
|
119
131
|
repoAlias: options?.repoAlias,
|
|
120
132
|
embeddings: options?.embeddings,
|
|
@@ -241,11 +253,12 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
241
253
|
// ── Phase 1: Full Pipeline (0–60%) ─────────────────────────────────
|
|
242
254
|
let pipelineResult;
|
|
243
255
|
try {
|
|
256
|
+
const pipelineRunOptions = buildPipelineRunOptionsForAnalyze({ includeExtensions, scopeRules }, options);
|
|
244
257
|
pipelineResult = await runPipelineFromRepo(repoPath, (progress) => {
|
|
245
258
|
const phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
|
|
246
259
|
const scaled = Math.round(progress.percent * 0.6);
|
|
247
260
|
updateBar(scaled, phaseLabel);
|
|
248
|
-
},
|
|
261
|
+
}, pipelineRunOptions);
|
|
249
262
|
}
|
|
250
263
|
catch (error) {
|
|
251
264
|
clearInterval(elapsedTimer);
|
|
@@ -391,7 +404,6 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
391
404
|
processes: pipelineRuntime.processResult?.stats.totalProcesses,
|
|
392
405
|
}, {
|
|
393
406
|
skillScope: (cliConfig.setupScope === 'global') ? 'global' : 'project',
|
|
394
|
-
cliPackageSpec: resolveCliSpec({ config: cliConfig }).packageSpec,
|
|
395
407
|
}, generatedSkills);
|
|
396
408
|
await closeLbug();
|
|
397
409
|
// Note: we intentionally do NOT call disposeEmbedder() here.
|
|
@@ -432,6 +444,10 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
432
444
|
for (const line of unitySummaryLines) {
|
|
433
445
|
console.log(` ${line}`);
|
|
434
446
|
}
|
|
447
|
+
const csharpPreprocSummaryLines = formatCSharpPreprocDiagnosticsSummary(pipelineRuntime.csharpPreprocDiagnostics);
|
|
448
|
+
for (const line of csharpPreprocSummaryLines) {
|
|
449
|
+
console.log(` ${line}`);
|
|
450
|
+
}
|
|
435
451
|
console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineRuntime.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineRuntime.processResult?.stats.totalProcesses || 0} flows`);
|
|
436
452
|
console.log(` LadybugDB ${lbugTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
|
|
437
453
|
if (includeExtensions.length > 0) {
|
|
@@ -460,6 +476,15 @@ export const analyzeCommand = async (inputPath, options) => {
|
|
|
460
476
|
// platforms (#38, #40). Force-exit to ensure clean termination.
|
|
461
477
|
process.exit(0);
|
|
462
478
|
};
|
|
479
|
+
export function buildPipelineRunOptionsForAnalyze(resolvedOptions, options) {
|
|
480
|
+
return {
|
|
481
|
+
includeExtensions: resolvedOptions.includeExtensions,
|
|
482
|
+
scopeRules: resolvedOptions.scopeRules,
|
|
483
|
+
...(options?.csharpDefineCsproj
|
|
484
|
+
? { csharpDefineCsproj: options.csharpDefineCsproj }
|
|
485
|
+
: {}),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
463
488
|
async function persistUnityParitySeed(storagePath, seed) {
|
|
464
489
|
const seedPath = path.join(storagePath, 'unity-parity-seed.json');
|
|
465
490
|
if (!seed) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { buildPipelineRunOptionsForAnalyze } from './analyze.js';
|
|
5
|
+
import { resolveScopeManifestForAnalyze } from './sync-manifest.js';
|
|
6
|
+
test('analyze auto-loads .gitnexus/sync-manifest.txt when CLI scope options are omitted', async () => {
|
|
7
|
+
const repoPath = path.join('/tmp', 'demo-repo');
|
|
8
|
+
const expectedManifest = path.join(repoPath, '.gitnexus', 'sync-manifest.txt');
|
|
9
|
+
const resolved = await resolveScopeManifestForAnalyze(repoPath, {}, async (candidate) => candidate === expectedManifest);
|
|
10
|
+
assert.equal(resolved, expectedManifest);
|
|
11
|
+
});
|
|
12
|
+
test('explicit --scope-manifest still wins over auto-detected default file', async () => {
|
|
13
|
+
const repoPath = path.join('/tmp', 'demo-repo');
|
|
14
|
+
const explicitManifest = path.join(repoPath, 'custom-manifest.txt');
|
|
15
|
+
const resolved = await resolveScopeManifestForAnalyze(repoPath, { scopeManifest: explicitManifest }, async () => true);
|
|
16
|
+
assert.equal(resolved, explicitManifest);
|
|
17
|
+
});
|
|
18
|
+
test('buildPipelineRunOptionsForAnalyze passes csharp define csproj option through to pipeline', () => {
|
|
19
|
+
const out = buildPipelineRunOptionsForAnalyze({ includeExtensions: ['.cs'], scopeRules: ['Assets/**'] }, { csharpDefineCsproj: '/tmp/Assembly-CSharp.csproj' });
|
|
20
|
+
assert.deepEqual(out, {
|
|
21
|
+
includeExtensions: ['.cs'],
|
|
22
|
+
scopeRules: ['Assets/**'],
|
|
23
|
+
csharpDefineCsproj: '/tmp/Assembly-CSharp.csproj',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -27,7 +27,7 @@ export async function benchmarkAgentContextCommand(dataset, options, deps) {
|
|
|
27
27
|
}
|
|
28
28
|
const analyzePath = path.resolve(options.targetPath);
|
|
29
29
|
const analyzeOptions = {
|
|
30
|
-
extensions: options.extensions
|
|
30
|
+
extensions: options.extensions,
|
|
31
31
|
repoAlias: options.repoAlias,
|
|
32
32
|
scopeManifest: options.scopeManifest,
|
|
33
33
|
scopePrefix: options.scopePrefix,
|
|
@@ -18,7 +18,7 @@ export async function benchmarkUnityCommand(dataset, options) {
|
|
|
18
18
|
targetPath: options.targetPath,
|
|
19
19
|
profile: profileConfig,
|
|
20
20
|
reportDir: options.reportDir,
|
|
21
|
-
extensions: options.extensions
|
|
21
|
+
extensions: options.extensions,
|
|
22
22
|
scopeManifest: options.scopeManifest,
|
|
23
23
|
scopePrefix: options.scopePrefix,
|
|
24
24
|
skipAnalyze: options.skipAnalyze ?? false,
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { resolveProfileConfig } from './benchmark-unity.js';
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packagePath = path.resolve(here, '..', '..', 'package.json');
|
|
5
9
|
test('quick profile uses reduced sample limits', () => {
|
|
6
10
|
const c = resolveProfileConfig('quick');
|
|
7
11
|
assert.equal(c.maxSymbols, 10);
|
|
8
12
|
assert.equal(c.maxTasks, 5);
|
|
9
13
|
});
|
|
10
14
|
test('package scripts include neonspark benchmark commands', async () => {
|
|
11
|
-
const raw = await fs.readFile(
|
|
15
|
+
const raw = await fs.readFile(packagePath, 'utf-8');
|
|
12
16
|
const pkg = JSON.parse(raw);
|
|
13
17
|
const scripts = pkg.scripts || {};
|
|
14
18
|
assert.ok(scripts['benchmark:neonspark:full']);
|
package/dist/cli/index.js
CHANGED
|
@@ -29,6 +29,8 @@ program
|
|
|
29
29
|
.option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
|
|
30
30
|
.option('--extensions <list>', 'Comma-separated file extensions to include (e.g. .cs,.ts)')
|
|
31
31
|
.option('--repo-alias <name>', 'Override indexed repository name with a stable alias')
|
|
32
|
+
.option('--csharp-define-csproj <path>', 'Load C# DefineConstants from the specified .csproj and normalize conditional-compilation blocks before parsing')
|
|
33
|
+
.option('--sync-manifest-policy <policy>', 'When CLI options differ from sync manifest directives: ask|update|keep|error (default: ask)')
|
|
32
34
|
.option('--skills', 'Generate repo-specific skill files from detected communities')
|
|
33
35
|
.option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
|
|
34
36
|
.option('--scope-manifest <path>', 'Manifest file with scope rules (supports comments and * wildcard; recommended: .gitnexus/sync-manifest.txt)')
|
|
@@ -150,7 +152,7 @@ program
|
|
|
150
152
|
.option('--repo-alias <name>', 'Analyze-time repo alias and default evaluation repo when --repo is omitted')
|
|
151
153
|
.option('--target-path <path>', 'Path to analyze before evaluation (required unless --skip-analyze)')
|
|
152
154
|
.option('--report-dir <path>', 'Output directory for benchmark-report.json and benchmark-summary.md', '.gitnexus/benchmark')
|
|
153
|
-
.option('--extensions <list>', 'Analyze extension filter (
|
|
155
|
+
.option('--extensions <list>', 'Analyze extension filter (comma-separated, optional)')
|
|
154
156
|
.option('--scope-manifest <path>', 'Analyze scope manifest file')
|
|
155
157
|
.option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
|
|
156
158
|
.option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
|
|
@@ -163,7 +165,7 @@ program
|
|
|
163
165
|
.option('--repo-alias <name>', 'Analyze-time repo alias and default evaluation repo when --repo is omitted')
|
|
164
166
|
.option('--target-path <path>', 'Path to analyze before evaluation (required unless --skip-analyze)')
|
|
165
167
|
.option('--report-dir <path>', 'Output directory for benchmark-report.json and benchmark-summary.md', '.gitnexus/benchmark-agent-context')
|
|
166
|
-
.option('--extensions <list>', 'Analyze extension filter (
|
|
168
|
+
.option('--extensions <list>', 'Analyze extension filter (comma-separated, optional)')
|
|
167
169
|
.option('--scope-manifest <path>', 'Analyze scope manifest file')
|
|
168
170
|
.option('--scope-prefix <pathPrefix>', 'Analyze scope path prefix (repeatable)', collectValues, [])
|
|
169
171
|
.option('--skip-analyze', 'Skip analyze stage and evaluate current index only')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { normalizeScopeRules, normalizeScopedPath } from '../core/ingestion/scope-filter.js';
|
|
2
|
+
const SUPPORTED_DIRECTIVES = new Set(['extensions', 'repoalias', 'embeddings']);
|
|
3
|
+
export function parseScopeManifestConfig(raw) {
|
|
4
|
+
const scopeRules = [];
|
|
5
|
+
const directives = {};
|
|
6
|
+
const lines = raw.split(/\r?\n/);
|
|
7
|
+
for (const [index, line] of lines.entries()) {
|
|
8
|
+
const trimmed = line.trim();
|
|
9
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
10
|
+
continue;
|
|
11
|
+
if (trimmed.startsWith('@')) {
|
|
12
|
+
const separatorIndex = trimmed.indexOf('=');
|
|
13
|
+
if (separatorIndex <= 1) {
|
|
14
|
+
throw new Error(`Invalid manifest directive at line ${index + 1}: ${trimmed}`);
|
|
15
|
+
}
|
|
16
|
+
const key = trimmed.slice(1, separatorIndex).trim().toLowerCase();
|
|
17
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
18
|
+
if (!SUPPORTED_DIRECTIVES.has(key)) {
|
|
19
|
+
throw new Error(`Unknown manifest directive: @${key}`);
|
|
20
|
+
}
|
|
21
|
+
if (key === 'extensions')
|
|
22
|
+
directives.extensions = value;
|
|
23
|
+
else if (key === 'repoalias')
|
|
24
|
+
directives.repoAlias = value;
|
|
25
|
+
else if (key === 'embeddings')
|
|
26
|
+
directives.embeddings = value;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const normalized = normalizeScopedPath(trimmed);
|
|
30
|
+
if (normalized)
|
|
31
|
+
scopeRules.push(normalized);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
scopeRules: normalizeScopeRules(scopeRules),
|
|
35
|
+
directives,
|
|
36
|
+
};
|
|
37
|
+
}
|