@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.
Files changed (54) hide show
  1. package/dist/benchmark/analyze-runner.d.ts +1 -1
  2. package/dist/benchmark/analyze-runner.js +4 -3
  3. package/dist/benchmark/analyze-runner.test.js +7 -0
  4. package/dist/cli/ai-context.d.ts +0 -1
  5. package/dist/cli/ai-context.js +15 -6
  6. package/dist/cli/analyze-options.js +58 -34
  7. package/dist/cli/analyze-options.test.js +57 -0
  8. package/dist/cli/analyze-runtime-summary.js +1 -0
  9. package/dist/cli/analyze-runtime-summary.test.js +10 -0
  10. package/dist/cli/analyze-summary.d.ts +2 -0
  11. package/dist/cli/analyze-summary.js +19 -0
  12. package/dist/cli/analyze.d.ts +11 -0
  13. package/dist/cli/analyze.js +30 -5
  14. package/dist/cli/analyze.test.d.ts +1 -0
  15. package/dist/cli/analyze.test.js +25 -0
  16. package/dist/cli/benchmark-agent-context.js +1 -1
  17. package/dist/cli/benchmark-unity.js +1 -1
  18. package/dist/cli/benchmark-unity.test.js +5 -1
  19. package/dist/cli/index.js +4 -2
  20. package/dist/cli/scope-manifest-config.d.ts +9 -0
  21. package/dist/cli/scope-manifest-config.js +37 -0
  22. package/dist/cli/setup.js +40 -41
  23. package/dist/cli/setup.test.js +14 -14
  24. package/dist/cli/sync-manifest.d.ts +27 -0
  25. package/dist/cli/sync-manifest.js +200 -0
  26. package/dist/cli/sync-manifest.test.d.ts +1 -0
  27. package/dist/cli/sync-manifest.test.js +88 -0
  28. package/dist/core/config/unity-config.d.ts +1 -0
  29. package/dist/core/config/unity-config.js +1 -0
  30. package/dist/core/ingestion/call-processor.d.ts +2 -1
  31. package/dist/core/ingestion/call-processor.js +28 -6
  32. package/dist/core/ingestion/heritage-processor.d.ts +2 -1
  33. package/dist/core/ingestion/heritage-processor.js +30 -7
  34. package/dist/core/ingestion/import-processor.d.ts +2 -1
  35. package/dist/core/ingestion/import-processor.js +28 -6
  36. package/dist/core/ingestion/parsing-processor.d.ts +5 -3
  37. package/dist/core/ingestion/parsing-processor.js +46 -13
  38. package/dist/core/ingestion/pipeline.js +65 -13
  39. package/dist/core/ingestion/unity-runtime-binding-rules.d.ts +1 -1
  40. package/dist/core/ingestion/unity-runtime-binding-rules.js +21 -18
  41. package/dist/core/ingestion/workers/parse-worker.d.ts +2 -0
  42. package/dist/core/ingestion/workers/parse-worker.js +50 -6
  43. package/dist/core/tree-sitter/csharp-define-profile.d.ts +6 -0
  44. package/dist/core/tree-sitter/csharp-define-profile.js +43 -0
  45. package/dist/core/tree-sitter/csharp-preproc-normalizer.d.ts +14 -0
  46. package/dist/core/tree-sitter/csharp-preproc-normalizer.js +261 -0
  47. package/dist/core/tree-sitter/parser-loader.d.ts +10 -0
  48. package/dist/core/tree-sitter/parser-loader.js +19 -0
  49. package/dist/types/pipeline.d.ts +13 -0
  50. package/package.json +12 -12
  51. package/scripts/check-sync-manifest-traceability.mjs +203 -0
  52. package/scripts/tree-sitter-audit-classify.mjs +172 -0
  53. package/skills/gitnexus-cli.md +36 -4
  54. package/skills/gitnexus-unity-rule-gen.md +2 -2
@@ -1,5 +1,5 @@
1
1
  export interface AnalyzeRunOptions {
2
- extensions: string;
2
+ extensions?: string;
3
3
  repoAlias?: string;
4
4
  scopeManifest?: string;
5
5
  scopePrefix?: string[];
@@ -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
+ });
@@ -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
  }>;
@@ -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, cliPackageSpec, generatedSkills) {
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 \`${reindexCmd}\` with that exact package spec (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.
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 cliPackageSpec = options?.cliPackageSpec || resolveCliSpec().packageSpec;
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, parseScopeRules } from '../core/ingestion/scope-filter.js';
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
- const rules = [];
25
+ let manifestRules = [];
25
26
  if (options?.scopeManifest) {
26
27
  const manifestPath = path.resolve(options.scopeManifest);
27
- let content;
28
- try {
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
- const normalizedRules = normalizeScopeRules(rules);
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 = await resolveAnalyzeScopeRules({
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
- : (canReuse ? (stored?.includeExtensions || []) : []);
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
- : (canReuse ? normalizeRepoAlias(stored?.repoAlias) : undefined);
84
- const embeddings = options?.embeddings ?? (canReuse ? Boolean(stored?.embeddings) : false);
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
+ });
@@ -5,5 +5,6 @@ export function toPipelineRuntimeSummary(input) {
5
5
  processResult: input.processResult,
6
6
  unityResult: input.unityResult,
7
7
  scopeDiagnostics: input.scopeDiagnostics,
8
+ csharpPreprocDiagnostics: input.csharpPreprocDiagnostics,
8
9
  };
9
10
  }
@@ -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 [];
@@ -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
+ };
@@ -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 { resolveCliSpec } from '../config/cli-spec.js';
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: options?.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
- }, { includeExtensions, scopeRules });
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 || '.cs',
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 || '.cs',
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('package.json', 'utf-8');
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 (default: .cs)', '.cs')
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 (default: .cs)', '.cs')
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,9 @@
1
+ export interface ScopeManifestConfig {
2
+ scopeRules: string[];
3
+ directives: {
4
+ extensions?: string;
5
+ repoAlias?: string;
6
+ embeddings?: string;
7
+ };
8
+ }
9
+ export declare function parseScopeManifestConfig(raw: string): ScopeManifestConfig;
@@ -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
+ }