gitnexus 1.6.7-rc.2 → 1.6.7-rc.4

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.
@@ -19,7 +19,7 @@ import { getGitRoot, hasGitDir, getDefaultBranch } from '../storage/git.js';
19
19
  import { loadAnalyzeConfig, mergeAnalyzeOptions, resolveDefaultBranch, validateBranchName, GitNexusRcError, } from './analyze-config.js';
20
20
  import { runFullAnalysis } from '../core/run-analyze.js';
21
21
  import { getMaxFileSizeBannerMessage } from '../core/ingestion/utils/max-file-size.js';
22
- import { warnMissingOptionalGrammars } from './optional-grammars.js';
22
+ import { warnMissingOptionalGrammars, getOptionalGrammarExtensions } from './optional-grammars.js';
23
23
  import { glob } from 'glob';
24
24
  import fs from 'fs/promises';
25
25
  import { cliError } from './cli-message.js';
@@ -691,11 +691,13 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
691
691
  console.log(' Note: --index-only overrides --skills; community skill files will not be written.\n');
692
692
  }
693
693
  // If the target repo contains files an optional grammar would parse but
694
- // that grammar's native binding is absent, warn before analysis so users
695
- // learn why those files end up unparsed instead of silently getting a
696
- // degraded index.
694
+ // that grammar's native binding is absent (or disabled via
695
+ // GITNEXUS_SKIP_OPTIONAL_GRAMMARS), warn before analysis so users learn why
696
+ // those files end up unparsed instead of silently getting a degraded index.
697
+ // The extension set is derived from OPTIONAL_GRAMMARS so it can't drift.
697
698
  try {
698
- const matches = await glob(['**/*.dart', '**/*.proto'], {
699
+ const optionalGlobs = getOptionalGrammarExtensions().map((e) => `**/*${e}`);
700
+ const matches = await glob(optionalGlobs, {
699
701
  cwd: repoPath,
700
702
  ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
701
703
  dot: false,
@@ -4,19 +4,33 @@
4
4
  * tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
5
5
  * under vendor/ and materialized into node_modules/ at postinstall. Dart
6
6
  * and Proto are built from source with node-gyp; Swift ships platform
7
- * prebuilds activated via node-gyp-build. All three can be skipped via
7
+ * prebuilds activated via node-gyp-build. tree-sitter-kotlin is a declared
8
+ * optionalDependency (not vendored). All can be skipped via
8
9
  * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
- * soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
10
- * matches the host platform (Swift).
10
+ * soft-fail when the toolchain is missing (Dart/Proto), when no prebuild
11
+ * matches the host platform (Swift), or when the optional install was
12
+ * skipped or its native build failed (Kotlin).
11
13
  *
12
14
  * Either path produces the same observable: the .node binding is absent
13
15
  * at runtime. This helper detects that condition and surfaces a single
14
- * stderr line per missing grammar so users learn why .dart/.proto/.swift
16
+ * stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt
15
17
  * support is unavailable instead of silently getting a degraded index.
16
18
  */
19
+ /**
20
+ * The file extensions backed by an optional grammar — the single source for
21
+ * the `analyze` preflight glob (so the glob can't drift from this list).
22
+ */
23
+ export declare function getOptionalGrammarExtensions(): string[];
17
24
  export interface MissingGrammar {
18
25
  name: string;
19
26
  extensions: string[];
27
+ /**
28
+ * `missing` — the native binding could not be loaded (not installed / build
29
+ * soft-failed / no prebuild). `skipped` — the binding is fine but the user
30
+ * disabled it via `GITNEXUS_SKIP_OPTIONAL_GRAMMARS`. Drives the warning text
31
+ * so a deliberate opt-out is not told to reinstall.
32
+ */
33
+ reason: 'missing' | 'skipped';
20
34
  }
21
35
  /**
22
36
  * Returns the list of optional grammars whose native binding cannot be
@@ -4,24 +4,51 @@
4
4
  * tree-sitter-dart, tree-sitter-proto, and tree-sitter-swift are vendored
5
5
  * under vendor/ and materialized into node_modules/ at postinstall. Dart
6
6
  * and Proto are built from source with node-gyp; Swift ships platform
7
- * prebuilds activated via node-gyp-build. All three can be skipped via
7
+ * prebuilds activated via node-gyp-build. tree-sitter-kotlin is a declared
8
+ * optionalDependency (not vendored). All can be skipped via
8
9
  * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently
9
- * soft-fail when the toolchain is missing (Dart/Proto) or no prebuild
10
- * matches the host platform (Swift).
10
+ * soft-fail when the toolchain is missing (Dart/Proto), when no prebuild
11
+ * matches the host platform (Swift), or when the optional install was
12
+ * skipped or its native build failed (Kotlin).
11
13
  *
12
14
  * Either path produces the same observable: the .node binding is absent
13
15
  * at runtime. This helper detects that condition and surfaces a single
14
- * stderr line per missing grammar so users learn why .dart/.proto/.swift
16
+ * stderr line per missing grammar so users learn why .dart/.proto/.swift/.kt
15
17
  * support is unavailable instead of silently getting a degraded index.
16
18
  */
17
19
  import { createRequire } from 'module';
20
+ import { SupportedLanguages } from '../_shared/index.js';
21
+ import { isGrammarRuntimeSkipped } from '../core/tree-sitter/parser-loader.js';
18
22
  import { cliWarn } from './cli-message.js';
19
23
  const _require = createRequire(import.meta.url);
20
24
  const OPTIONAL_GRAMMARS = [
21
- { name: 'tree-sitter-dart', pkg: 'tree-sitter-dart', extensions: ['.dart'] },
25
+ {
26
+ name: 'tree-sitter-dart',
27
+ pkg: 'tree-sitter-dart',
28
+ extensions: ['.dart'],
29
+ language: SupportedLanguages.Dart,
30
+ },
22
31
  { name: 'tree-sitter-proto', pkg: 'tree-sitter-proto', extensions: ['.proto'] },
23
- { name: 'tree-sitter-swift', pkg: 'tree-sitter-swift', extensions: ['.swift'] },
32
+ {
33
+ name: 'tree-sitter-swift',
34
+ pkg: 'tree-sitter-swift',
35
+ extensions: ['.swift'],
36
+ language: SupportedLanguages.Swift,
37
+ },
38
+ {
39
+ name: 'tree-sitter-kotlin',
40
+ pkg: 'tree-sitter-kotlin',
41
+ extensions: ['.kt', '.kts'],
42
+ language: SupportedLanguages.Kotlin,
43
+ },
24
44
  ];
45
+ /**
46
+ * The file extensions backed by an optional grammar — the single source for
47
+ * the `analyze` preflight glob (so the glob can't drift from this list).
48
+ */
49
+ export function getOptionalGrammarExtensions() {
50
+ return [...new Set(OPTIONAL_GRAMMARS.flatMap((g) => g.extensions))];
51
+ }
25
52
  /**
26
53
  * Returns the list of optional grammars whose native binding cannot be
27
54
  * loaded. Actually `require()`s the package — `require.resolve` would
@@ -41,6 +68,13 @@ const OPTIONAL_GRAMMARS = [
41
68
  export function detectMissingOptionalGrammars() {
42
69
  const missing = [];
43
70
  for (const g of OPTIONAL_GRAMMARS) {
71
+ // Deliberate runtime opt-out comes first: even an installed binding is
72
+ // treated as unavailable, with a `skipped` reason so the warning says so
73
+ // instead of suggesting a reinstall (#2101 review).
74
+ if (g.language !== undefined && isGrammarRuntimeSkipped(g.language)) {
75
+ missing.push({ name: g.name, extensions: g.extensions, reason: 'skipped' });
76
+ continue;
77
+ }
44
78
  try {
45
79
  _require(g.pkg);
46
80
  }
@@ -59,7 +93,7 @@ export function detectMissingOptionalGrammars() {
59
93
  // there for tool-data stdout output).
60
94
  cliWarn(`GitNexus: optional grammar "${g.name}" is installed but failed to load (${msg.slice(0, 200)}). ${g.extensions.join('/')} files will not be parsed.`, { grammar: g.name, extensions: g.extensions, error: msg });
61
95
  }
62
- missing.push({ name: g.name, extensions: g.extensions });
96
+ missing.push({ name: g.name, extensions: g.extensions, reason: 'missing' });
63
97
  }
64
98
  }
65
99
  return missing;
@@ -86,6 +120,15 @@ export function warnMissingOptionalGrammars(opts) {
86
120
  if (relevantExtensions && !g.extensions.some((e) => relevantExtensions.has(e))) {
87
121
  continue;
88
122
  }
89
- cliWarn(`GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${g.extensions.join('/')} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`, { grammar: g.name, extensions: g.extensions, context: opts?.context });
123
+ const exts = g.extensions.join('/');
124
+ const message = g.reason === 'skipped'
125
+ ? `GitNexus${ctx}: optional grammar "${g.name}" is disabled via GITNEXUS_SKIP_OPTIONAL_GRAMMARS — ${exts} files will not be parsed. Unset the variable to re-enable.`
126
+ : `GitNexus${ctx}: optional grammar "${g.name}" is unavailable — ${exts} files will not be parsed. Reinstall without GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (and ensure python3, make, g++) to enable.`;
127
+ cliWarn(message, {
128
+ grammar: g.name,
129
+ extensions: g.extensions,
130
+ reason: g.reason,
131
+ context: opts?.context,
132
+ });
90
133
  }
91
134
  }
@@ -22,7 +22,15 @@
22
22
  * - Parameter / return / receiver type bindings and arity metadata.
23
23
  */
24
24
  import Parser from 'tree-sitter';
25
- import Dart from 'tree-sitter-dart';
25
+ import { SupportedLanguages } from '../../../../_shared/index.js';
26
+ // `tree-sitter-dart` is an optional/vendored grammar that may be absent on a
27
+ // default install. Loaded lazily + guarded via parser-loader rather than
28
+ // statically imported: this module is pulled onto the main thread eagerly by
29
+ // the scope-resolution registry and the language-provider index, so a top-level
30
+ // `import Dart from 'tree-sitter-dart'` would throw ERR_MODULE_NOT_FOUND at
31
+ // module-load and crash `analyze` even for repos with no Dart files (#2091,
32
+ // #2093). The grammar is only ever needed inside the lazy getters below.
33
+ import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
26
34
  const DART_SCOPE_QUERY = `
27
35
  ; ── Scopes ───────────────────────────────────────────────────────────────────
28
36
  (program) @scope.module
@@ -130,13 +138,13 @@ let _query = null;
130
138
  export function getDartParser() {
131
139
  if (_parser === null) {
132
140
  _parser = new Parser();
133
- _parser.setLanguage(Dart);
141
+ _parser.setLanguage(getLanguageGrammar(SupportedLanguages.Dart));
134
142
  }
135
143
  return _parser;
136
144
  }
137
145
  export function getDartScopeQuery() {
138
146
  if (_query === null) {
139
- _query = new Parser.Query(Dart, DART_SCOPE_QUERY);
147
+ _query = new Parser.Query(getLanguageGrammar(SupportedLanguages.Dart), DART_SCOPE_QUERY);
140
148
  }
141
149
  return _query;
142
150
  }
@@ -1,5 +1,13 @@
1
1
  import Parser from 'tree-sitter';
2
- import Kotlin from 'tree-sitter-kotlin';
2
+ import { SupportedLanguages } from '../../../../_shared/index.js';
3
+ // `tree-sitter-kotlin` is an optionalDependency that may be absent on a default
4
+ // install (or fail its native build). Loaded lazily + guarded via parser-loader
5
+ // rather than statically imported: this module is pulled onto the main thread
6
+ // eagerly by the scope-resolution registry and the language-provider index, so
7
+ // a top-level `import Kotlin from 'tree-sitter-kotlin'` would throw
8
+ // ERR_MODULE_NOT_FOUND at module-load and crash `analyze` even for repos with no
9
+ // Kotlin files (#2091, #2093). The grammar is only ever needed in the getters.
10
+ import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
3
11
  const KOTLIN_SCOPE_QUERY = `
4
12
  ;; Scopes
5
13
  (source_file) @scope.module
@@ -176,13 +184,13 @@ let query = null;
176
184
  export function getKotlinParser() {
177
185
  if (parser === null) {
178
186
  parser = new Parser();
179
- parser.setLanguage(Kotlin);
187
+ parser.setLanguage(getLanguageGrammar(SupportedLanguages.Kotlin));
180
188
  }
181
189
  return parser;
182
190
  }
183
191
  export function getKotlinScopeQuery() {
184
192
  if (query === null) {
185
- query = new Parser.Query(Kotlin, KOTLIN_SCOPE_QUERY);
193
+ query = new Parser.Query(getLanguageGrammar(SupportedLanguages.Kotlin), KOTLIN_SCOPE_QUERY);
186
194
  }
187
195
  return query;
188
196
  }
@@ -42,7 +42,15 @@
42
42
  * tree-sitter init cost per file.
43
43
  */
44
44
  import Parser from 'tree-sitter';
45
- import Swift from 'tree-sitter-swift';
45
+ import { SupportedLanguages } from '../../../../_shared/index.js';
46
+ // `tree-sitter-swift` is an optional/vendored grammar that may be absent on a
47
+ // default install. It is loaded lazily + guarded via parser-loader rather than
48
+ // statically imported: this module is pulled onto the main thread eagerly by
49
+ // the scope-resolution registry and the language-provider index, so a top-level
50
+ // `import Swift from 'tree-sitter-swift'` would throw ERR_MODULE_NOT_FOUND at
51
+ // module-load and crash `analyze` even for repos with no Swift files (#2091,
52
+ // #2093). The grammar is only ever needed inside the lazy getters below.
53
+ import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js';
46
54
  const SWIFT_SCOPE_QUERY = `
47
55
  ;; ── Scopes ──────────────────────────────────────────────────────────
48
56
  (source_file) @scope.module
@@ -182,13 +190,13 @@ let _query = null;
182
190
  export function getSwiftParser() {
183
191
  if (_parser === null) {
184
192
  _parser = new Parser();
185
- _parser.setLanguage(Swift);
193
+ _parser.setLanguage(getLanguageGrammar(SupportedLanguages.Swift));
186
194
  }
187
195
  return _parser;
188
196
  }
189
197
  export function getSwiftScopeQuery() {
190
198
  if (_query === null) {
191
- _query = new Parser.Query(Swift, SWIFT_SCOPE_QUERY);
199
+ _query = new Parser.Query(getLanguageGrammar(SupportedLanguages.Swift), SWIFT_SCOPE_QUERY);
192
200
  }
193
201
  return _query;
194
202
  }
@@ -20,9 +20,9 @@ import { fileContentHash, computeChunkHash, loadParseCacheChunk, persistParseCac
20
20
  import { clearParsedFileStore, persistParsedFileChunk, getDurableParsedFileDir, loadDurableParsedFileIndex, restoreDurableParsedFileShard, } from '../../../storage/parsedfile-store.js';
21
21
  import { processRoutesFromExtracted, buildExportedTypeMapFromGraph, } from '../call-processor.js';
22
22
  import { createSemanticModel } from '../model/index.js';
23
- import { getLanguageFromFilename } from '../../../_shared/index.js';
23
+ import { getLanguageFromFilename, } from '../../../_shared/index.js';
24
24
  import { readFileContents } from '../filesystem-walker.js';
25
- import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
25
+ import { isLanguageAvailable, isGrammarRuntimeSkipped } from '../../tree-sitter/parser-loader.js';
26
26
  import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
27
27
  import fs from 'node:fs';
28
28
  import path from 'node:path';
@@ -167,7 +167,15 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
167
167
  }
168
168
  }
169
169
  for (const [lang, count] of skippedByLang) {
170
- logger.warn(`Skipping ${count} ${lang} file(s) ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`);
170
+ // Distinguish a deliberate runtime opt-out from a genuinely-missing binding
171
+ // so we don't tell a user who set GITNEXUS_SKIP_OPTIONAL_GRAMMARS to
172
+ // `npm rebuild` a grammar that built fine (#2091/#2093 review).
173
+ if (isGrammarRuntimeSkipped(lang)) {
174
+ logger.warn(`Skipping ${count} ${lang} file(s) — ${lang} parsing disabled via GITNEXUS_SKIP_OPTIONAL_GRAMMARS.`);
175
+ }
176
+ else {
177
+ logger.warn(`Skipping ${count} ${lang} file(s) — ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`);
178
+ }
171
179
  }
172
180
  // Sort parseableScanned alphabetically for stable chunk membership
173
181
  // across runs (Finding 4). Without this, filesystem-scan order can
@@ -27,6 +27,7 @@ import { getPhaseOutput } from '../../pipeline-phases/types.js';
27
27
  import { getLanguageFromFilename } from '../../../../_shared/index.js';
28
28
  import { readFileContents } from '../../filesystem-walker.js';
29
29
  import { runScopeResolution } from './run.js';
30
+ import { isLanguageAvailable } from '../../../tree-sitter/parser-loader.js';
30
31
  import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
31
32
  import { SCOPE_RESOLVERS } from './registry.js';
32
33
  import { isDev, isSemanticModelValidatorEnabled } from '../../utils/env.js';
@@ -120,6 +121,16 @@ export const scopeResolutionPhase = {
120
121
  const fileLang = getLanguageFromFilename(f.path);
121
122
  if (fileLang === null)
122
123
  continue;
124
+ // Skip files whose grammar isn't available (optional grammars like
125
+ // swift/dart/kotlin on an install where the binding is absent or the
126
+ // user set GITNEXUS_SKIP_OPTIONAL_GRAMMARS). The parse phase already
127
+ // excluded and warned about these (parse-impl.ts); without this guard the
128
+ // file would fall through to the main-thread re-extract in run.ts and
129
+ // throw "Unsupported language" (caught, but noisy, and it needlessly
130
+ // loads the grammar on the main thread). `isLanguageAvailable` is
131
+ // memoized, so this stays O(1) per language. (#2091, #2093)
132
+ if (!isLanguageAvailable(fileLang))
133
+ continue;
123
134
  let bucket = filesByLang.get(fileLang);
124
135
  if (bucket === undefined) {
125
136
  bucket = [];
@@ -18,6 +18,19 @@ import { getProvider } from '../languages/index.js';
18
18
  import { getTreeSitterBufferSize, getTreeSitterContentByteLength, TREE_SITTER_MAX_BUFFER, } from '../constants.js';
19
19
  import { ARRAY_METHOD_HOC_BLOCKLIST_SET, DEFAULT_EXPORT_IDENTIFIER_BLOCKLIST_SET, deriveDefaultExportHocName, } from '../ts-js-hoc-utils.js';
20
20
  import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
21
+ // ── Worker grammar loading — enforcement boundary (#2091/#2093, #2101) ───────
22
+ // The worker maintains its own grammar table (the guarded `_require`s below +
23
+ // `languageMap`) and intentionally does NOT consult the runtime
24
+ // `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` opt-out. It does not need to: the MAIN
25
+ // THREAD's `parseableScanned` filter (pipeline-phases/parse-impl.ts, gated on
26
+ // `parser-loader.isLanguageAvailable`, which honors the runtime opt-out and a
27
+ // genuinely-absent binding alike) excludes files of an unavailable/opted-out
28
+ // language BEFORE any chunk is dispatched, so the worker never receives them.
29
+ // That main-thread filter is the single enforcement point. Any future change
30
+ // that dispatches files to the worker WITHOUT first passing them through
31
+ // `isLanguageAvailable` must re-introduce the gate here. (The cleaner end-state
32
+ // — routing this table through `parser-loader.getLanguageGrammar` so there is
33
+ // one loader — is the deferred Tier-1 consolidation.)
21
34
  // tree-sitter-swift is an optionalDependency — may not be installed
22
35
  const _require = createRequire(import.meta.url);
23
36
  let Swift = null;
@@ -15,6 +15,14 @@ export interface GrammarSourceDescriptor {
15
15
  export declare const listGrammarSources: () => GrammarSourceDescriptor[];
16
16
  export declare const resolveLanguageKey: (language: SupportedLanguages, filePath?: string) => string;
17
17
  export declare const isLanguageAvailable: (language: SupportedLanguages, filePath?: string) => boolean;
18
+ /**
19
+ * True when `language`'s grammar is being treated as unavailable specifically
20
+ * because of the runtime GITNEXUS_SKIP_OPTIONAL_GRAMMARS opt-out — as opposed
21
+ * to a genuinely-missing/broken native binding. Lets callers surface an
22
+ * accurate "skipped on purpose" message instead of a spurious "npm rebuild"
23
+ * recovery hint. Returns false for required grammars and for an absent env.
24
+ */
25
+ export declare const isGrammarRuntimeSkipped: (language: SupportedLanguages, filePath?: string) => boolean;
18
26
  export declare const getLanguageGrammar: (language: SupportedLanguages, filePath?: string) => unknown;
19
27
  export declare const loadParser: () => Promise<Parser>;
20
28
  export declare const loadLanguage: (language: SupportedLanguages, filePath?: string) => Promise<void>;
@@ -86,6 +86,7 @@ const SOURCES = {
86
86
  [SupportedLanguages.Swift]: {
87
87
  load: () => _require('tree-sitter-swift'),
88
88
  optional: true,
89
+ userSkippable: true,
89
90
  unavailableNote: 'Swift parsing disabled: vendored `tree-sitter-swift` (under ' +
90
91
  '`gitnexus/vendor/tree-sitter-swift`) failed to load. ' +
91
92
  'Likely cause: no prebuilt `.node` for this platform/architecture. ' +
@@ -94,6 +95,7 @@ const SOURCES = {
94
95
  [SupportedLanguages.Dart]: {
95
96
  load: () => _require('tree-sitter-dart'),
96
97
  optional: true,
98
+ userSkippable: true,
97
99
  unavailableNote: 'Dart parsing disabled: vendored `tree-sitter-dart` (under ' +
98
100
  '`gitnexus/vendor/tree-sitter-dart`) failed to load. ' +
99
101
  'Likely cause: native compile failed at install (missing python3/make/g++). ' +
@@ -102,6 +104,7 @@ const SOURCES = {
102
104
  [SupportedLanguages.Kotlin]: {
103
105
  load: () => _require('tree-sitter-kotlin'),
104
106
  optional: true,
107
+ userSkippable: true,
105
108
  unavailableNote: 'Kotlin parsing disabled: `tree-sitter-kotlin` is an optionalDependency ' +
106
109
  'and is not installed (or its native binding failed to build).',
107
110
  },
@@ -112,6 +115,43 @@ export const listGrammarSources = () => Object.entries(SOURCES).map(([key, sourc
112
115
  }));
113
116
  const loadCache = new Map();
114
117
  const logged = new Set();
118
+ // Parsed form of GITNEXUS_SKIP_OPTIONAL_GRAMMARS, resolved lazily ONCE per
119
+ // process. The env is set before analyze runs, so re-reading + re-allocating a
120
+ // Set on every call was wasted work (and a latent trap for any future per-file
121
+ // caller). `vi.resetModules()` gives the unit tests a fresh module — and thus a
122
+ // fresh memo — per case, so this stays test-friendly.
123
+ // 'all' → every userSkippable grammar; Set → only the named ids
124
+ // (and `tree-sitter-<id>` spellings); null → env unset/empty (nothing).
125
+ let _skipDirective;
126
+ const skipDirective = () => {
127
+ if (_skipDirective !== undefined)
128
+ return _skipDirective;
129
+ const raw = (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS ?? '').trim().toLowerCase();
130
+ if (raw === '')
131
+ return (_skipDirective = null);
132
+ if (raw === '1' || raw === 'true' || raw === 'all' || raw === '*')
133
+ return (_skipDirective = 'all');
134
+ return (_skipDirective = new Set(raw
135
+ .split(',')
136
+ .map((s) => s.trim())
137
+ .filter(Boolean)
138
+ .flatMap((s) => [s, s.replace(/^tree-sitter-/, '')])));
139
+ };
140
+ const isRuntimeSkippedGrammar = (key, source) => {
141
+ // Only grammars explicitly flagged user-skippable (swift/dart/kotlin) — never
142
+ // required deps that use the optional machinery for ABI safety (C carries no
143
+ // `userSkippable`).
144
+ if (source.userSkippable !== true)
145
+ return false;
146
+ const directive = skipDirective();
147
+ if (directive === null)
148
+ return false;
149
+ if (directive === 'all')
150
+ return true;
151
+ // `key` is the SupportedLanguages value (e.g. `swift`); the directive Set
152
+ // already holds both the bare id and the `tree-sitter-<id>` spelling.
153
+ return directive.has(key) || directive.has(`tree-sitter-${key}`);
154
+ };
115
155
  const logFailure = (key, result) => {
116
156
  if (result.ok === true)
117
157
  return;
@@ -148,6 +188,24 @@ const loadGrammar = (key) => {
148
188
  loadCache.set(key, result);
149
189
  return result;
150
190
  }
191
+ // Runtime opt-out: treat a user-skipped optional grammar exactly like an
192
+ // absent binding (non-fatal unavailable + one warning), without attempting
193
+ // the native load. See `isRuntimeSkippedGrammar`.
194
+ if (isRuntimeSkippedGrammar(key, source)) {
195
+ // Deliberate opt-out: emit an accurate "disabled on purpose" note rather
196
+ // than `source.unavailableNote` (which blames a missing/unbuilt binding and
197
+ // would mislead a user who set the env intentionally — #2101 review).
198
+ const result = {
199
+ ok: false,
200
+ error: new Error('runtime opt-out'),
201
+ note: `${key} parsing disabled via GITNEXUS_SKIP_OPTIONAL_GRAMMARS (unset it to re-enable).`,
202
+ fatal: false,
203
+ severity: 'warn',
204
+ };
205
+ loadCache.set(key, result);
206
+ logFailure(key, result);
207
+ return result;
208
+ }
151
209
  let result;
152
210
  try {
153
211
  result = { ok: true, grammar: source.load() };
@@ -168,6 +226,18 @@ const loadGrammar = (key) => {
168
226
  return result;
169
227
  };
170
228
  export const isLanguageAvailable = (language, filePath) => loadGrammar(resolveLanguageKey(language, filePath)).ok;
229
+ /**
230
+ * True when `language`'s grammar is being treated as unavailable specifically
231
+ * because of the runtime GITNEXUS_SKIP_OPTIONAL_GRAMMARS opt-out — as opposed
232
+ * to a genuinely-missing/broken native binding. Lets callers surface an
233
+ * accurate "skipped on purpose" message instead of a spurious "npm rebuild"
234
+ * recovery hint. Returns false for required grammars and for an absent env.
235
+ */
236
+ export const isGrammarRuntimeSkipped = (language, filePath) => {
237
+ const key = resolveLanguageKey(language, filePath);
238
+ const source = SOURCES[key];
239
+ return source !== undefined && isRuntimeSkippedGrammar(key, source);
240
+ };
171
241
  export const getLanguageGrammar = (language, filePath) => {
172
242
  const key = resolveLanguageKey(language, filePath);
173
243
  const result = loadGrammar(key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.7-rc.2",
3
+ "version": "1.6.7-rc.4",
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",