gitnexus 1.6.3-rc.41 → 1.6.3-rc.42

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/cli/index.js CHANGED
@@ -30,7 +30,9 @@ program
30
30
  .option('--max-file-size <kb>', 'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).')
31
31
  .addHelpText('after', '\nEnvironment variables:\n' +
32
32
  ' GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)\n' +
33
- ' GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.')
33
+ ' GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n' +
34
+ '\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n' +
35
+ ' `!__tests__/` to index a directory that is auto-filtered by default (#771).')
34
36
  .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
35
37
  program
36
38
  .command('index [path...]')
@@ -19,6 +19,15 @@ export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions
19
19
  *
20
20
  * Returns an IgnoreLike object for glob's `ignore` option,
21
21
  * enabling directory-level pruning during traversal.
22
+ *
23
+ * Precedence (#771): user's `.gitnexusignore` negation patterns take
24
+ * priority over the hardcoded list, matching `.gitignore` semantics.
25
+ * An explicit `!pattern` rule unignores descendants even when they
26
+ * would otherwise be blocked by DEFAULT_IGNORE_LIST — UNLESS a more
27
+ * specific rule in the same file re-ignores a subset (e.g.
28
+ * `!__tests__/` paired with `__tests__/generated/` blocks the child
29
+ * while leaving the parent negated). Last-match-wins is enforced by
30
+ * consulting `ig.ignores(rel)` after `hasExplicitUnignore`.
22
31
  */
23
32
  export declare const createIgnoreFilter: (repoPath: string, options?: IgnoreOptions) => Promise<{
24
33
  ignored(p: Path): boolean;
@@ -245,16 +245,20 @@ const IGNORED_FILES = new Set([
245
245
  '.env.test',
246
246
  '.env.example',
247
247
  ]);
248
- // NOTE: Negation patterns in .gitnexusignore (e.g. `!vendor/`) cannot override
249
- // entries in DEFAULT_IGNORE_LIST this is intentional. The hardcoded list protects
250
- // against indexing directories that are almost never source code (node_modules, .git, etc.).
251
- // Users who need to include such directories should remove them from the hardcoded list.
248
+ // The hardcoded DEFAULT_IGNORE_LIST is the "safety net" default: directories
249
+ // that are almost never source code (node_modules, .git, dist, __tests__,
250
+ // etc.). Users who legitimately need to index one of these can negate the
251
+ // hardcoded rule via a `!pattern` line in `.gitnexusignore` (#771) same
252
+ // semantics as `.gitignore` negation. That override is applied in
253
+ // `createIgnoreFilter` below; `shouldIgnorePath` itself stays a pure
254
+ // hardcoded-list check so its callers (wiki generator, tests) get
255
+ // deterministic results independent of per-repo config.
252
256
  export const shouldIgnorePath = (filePath) => {
253
257
  const normalizedPath = filePath.replace(/\\/g, '/');
254
258
  const parts = normalizedPath.split('/');
255
259
  const fileName = parts[parts.length - 1];
256
260
  const fileNameLower = fileName.toLowerCase();
257
- // Check if any path segment is in ignore list
261
+ // Check if any path segment is in the hardcoded ignore list.
258
262
  for (const part of parts) {
259
263
  if (DEFAULT_IGNORE_LIST.has(part)) {
260
264
  return true;
@@ -320,6 +324,44 @@ export const loadIgnoreRules = async (repoPath, options) => {
320
324
  }
321
325
  return hasRules ? ig : null;
322
326
  };
327
+ /**
328
+ * Walk ancestor segments of `rel` and check whether `.gitnexusignore`
329
+ * (or `.gitignore`) contains an explicit `!pattern` negation that
330
+ * applies. Returns true as soon as any segment — or the path itself —
331
+ * is matched by a negation rule.
332
+ *
333
+ * Why this exists (#771): the hardcoded DEFAULT_IGNORE_LIST would
334
+ * otherwise block indexing of directories like `__tests__/` even when
335
+ * the user has an explicit `!__tests__/` line in `.gitnexusignore`.
336
+ * Mirroring `.gitignore` negation semantics: a user's explicit
337
+ * unignore of a parent directory implicitly unignores everything
338
+ * underneath, so we walk the ancestor chain rather than only testing
339
+ * the leaf.
340
+ *
341
+ * The `ignore` package's `test(path)` returns `{ignored, unignored}`;
342
+ * `unignored: true` is the "a negation rule matched this path"
343
+ * signal. Children of a negated directory return
344
+ * `{ignored: false, unignored: false}` on a direct test, which is why
345
+ * we also walk the ancestors here.
346
+ */
347
+ const hasExplicitUnignore = (ig, rel) => {
348
+ // Direct match on the path (as a file).
349
+ if (ig.test(rel).unignored)
350
+ return true;
351
+ // Direct match on the path treated as a directory — `!dir/` matches
352
+ // here when rel is the directory itself.
353
+ if (ig.test(rel + '/').unignored)
354
+ return true;
355
+ // Walk ancestor segments. `!parent/` should propagate to every
356
+ // descendant the same way `.gitignore` negation propagates.
357
+ const parts = rel.split('/');
358
+ for (let i = parts.length - 1; i > 0; i--) {
359
+ const ancestor = parts.slice(0, i).join('/') + '/';
360
+ if (ig.test(ancestor).unignored)
361
+ return true;
362
+ }
363
+ return false;
364
+ };
323
365
  /**
324
366
  * Create a glob-compatible ignore filter combining:
325
367
  * - .gitignore / .gitnexusignore patterns (via `ignore` package)
@@ -327,6 +369,15 @@ export const loadIgnoreRules = async (repoPath, options) => {
327
369
  *
328
370
  * Returns an IgnoreLike object for glob's `ignore` option,
329
371
  * enabling directory-level pruning during traversal.
372
+ *
373
+ * Precedence (#771): user's `.gitnexusignore` negation patterns take
374
+ * priority over the hardcoded list, matching `.gitignore` semantics.
375
+ * An explicit `!pattern` rule unignores descendants even when they
376
+ * would otherwise be blocked by DEFAULT_IGNORE_LIST — UNLESS a more
377
+ * specific rule in the same file re-ignores a subset (e.g.
378
+ * `!__tests__/` paired with `__tests__/generated/` blocks the child
379
+ * while leaving the parent negated). Last-match-wins is enforced by
380
+ * consulting `ig.ignores(rel)` after `hasExplicitUnignore`.
330
381
  */
331
382
  export const createIgnoreFilter = async (repoPath, options) => {
332
383
  const ig = await loadIgnoreRules(repoPath, options);
@@ -337,6 +388,15 @@ export const createIgnoreFilter = async (repoPath, options) => {
337
388
  const rel = p.relative();
338
389
  if (!rel)
339
390
  return false;
391
+ // User's .gitnexusignore negation takes precedence over hardcoded
392
+ // rules (#771). If any ancestor or the path itself was explicitly
393
+ // unignored AND no more-specific rule re-ignores this exact path,
394
+ // allow it through. The `!ig.ignores(rel)` guard matches
395
+ // .gitignore's last-match-wins semantics: `!__tests__/` followed
396
+ // by `__tests__/generated/` negates the parent but still blocks
397
+ // the re-ignored child.
398
+ if (ig && hasExplicitUnignore(ig, rel) && !ig.ignores(rel))
399
+ return false;
340
400
  // Check .gitignore / .gitnexusignore patterns
341
401
  if (ig && ig.ignores(rel))
342
402
  return true;
@@ -344,10 +404,20 @@ export const createIgnoreFilter = async (repoPath, options) => {
344
404
  return shouldIgnorePath(rel);
345
405
  },
346
406
  childrenIgnored(p) {
347
- // Fast path: check directory name against hardcoded list.
348
407
  // Note: dot-directories (.git, .vscode, etc.) are primarily excluded by
349
- // glob's `dot: false` option in filesystem-walker.ts. This check is
350
- // defense-in-depth — do not remove `dot: false` assuming this covers it.
408
+ // glob's `dot: false` option in filesystem-walker.ts. The hardcoded
409
+ // list check below is defense-in-depth — do not remove `dot: false`
410
+ // assuming this covers it.
411
+ const rel = p.relative();
412
+ // User's .gitnexusignore negation takes precedence (#771) — if the
413
+ // user explicitly unignored this directory or any ancestor via a
414
+ // !pattern rule, allow descent even if the directory name is in
415
+ // DEFAULT_IGNORE_LIST. The `!ig.ignores(rel + '/')` guard keeps
416
+ // last-match-wins: `!__tests__/` + `__tests__/generated/` still
417
+ // blocks descent into `__tests__/generated/`.
418
+ if (ig && rel && hasExplicitUnignore(ig, rel) && !ig.ignores(rel + '/'))
419
+ return false;
420
+ // Hardcoded list: block descent into well-known noise directories.
351
421
  if (DEFAULT_IGNORE_LIST.has(p.name))
352
422
  return true;
353
423
  // Check against .gitignore / .gitnexusignore patterns.
@@ -358,11 +428,8 @@ export const createIgnoreFilter = async (repoPath, options) => {
358
428
  // Bare-name patterns (e.g. `local`) still match `local/` per gitignore spec:
359
429
  // the `ignore` package normalizes `dir` and `dir/` to match directories.
360
430
  // See: https://github.com/kaelzhang/node-ignore#2-filenames-and-dirnames
361
- if (ig) {
362
- const rel = p.relative();
363
- if (rel && ig.ignores(rel + '/'))
364
- return true;
365
- }
431
+ if (ig && rel && ig.ignores(rel + '/'))
432
+ return true;
366
433
  return false;
367
434
  },
368
435
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.3-rc.41",
3
+ "version": "1.6.3-rc.42",
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",