gitnexus 1.6.13-rc.69 → 1.6.13-rc.70
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/skill-gen.js
CHANGED
|
@@ -65,18 +65,18 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
65
65
|
/* mirror root may not exist yet */
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
const memberships = communityResult?.rawMemberships ?? communityResult?.memberships ?? [];
|
|
69
|
+
if (!communityResult || !memberships.length) {
|
|
69
70
|
console.log('\n Skills: no communities detected, skipping skill generation');
|
|
70
71
|
return { skills: [], outputPath: outputDir };
|
|
71
72
|
}
|
|
72
73
|
console.log('\n Generating repo-specific skills...');
|
|
73
|
-
// Step 1:
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// can be empty while memberships still has useful groupings.
|
|
74
|
+
// Step 1: Use raw assignments for the fallback when all communities were
|
|
75
|
+
// filtered as singletons. Same-folder aggregation can still produce skills
|
|
76
|
+
// for these sparse graphs without emitting dangling MEMBER_OF edges.
|
|
77
77
|
const communities = communityResult.communities.length > 0
|
|
78
78
|
? communityResult.communities
|
|
79
|
-
: buildCommunitiesFromMemberships(
|
|
79
|
+
: buildCommunitiesFromMemberships(memberships, graph, repoPath);
|
|
80
80
|
const aggregated = aggregateCommunities(communities);
|
|
81
81
|
// Step 2: Filter to significant communities
|
|
82
82
|
// Keep communities with >= 3 symbols after aggregation.
|
|
@@ -89,8 +89,8 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
89
89
|
return { skills: [], outputPath: outputDir };
|
|
90
90
|
}
|
|
91
91
|
// Step 3: Build lookup maps
|
|
92
|
-
const membershipsByComm = buildMembershipMap(
|
|
93
|
-
const nodeIdToCommunityLabel = buildNodeCommunityLabelMap(
|
|
92
|
+
const membershipsByComm = buildMembershipMap(memberships);
|
|
93
|
+
const nodeIdToCommunityLabel = buildNodeCommunityLabelMap(memberships, communities);
|
|
94
94
|
// Step 4: Ensure the shared project-skill root exists. Never clear it: it
|
|
95
95
|
// also contains user-authored and standard GitNexus skills.
|
|
96
96
|
await fs.mkdir(outputDir, { recursive: true });
|
|
@@ -120,7 +120,7 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
|
|
|
120
120
|
// Gather entry points
|
|
121
121
|
const entryPoints = gatherEntryPoints(members);
|
|
122
122
|
// Gather execution flows
|
|
123
|
-
const flows = gatherFlows(community.rawIds, processResult?.processes || []);
|
|
123
|
+
const flows = gatherFlows(community.rawIds, members, processResult?.processes || []);
|
|
124
124
|
// Gather cross-community connections
|
|
125
125
|
const connections = gatherCrossConnections(community.rawIds, community.label, membershipsByComm, nodeIdToCommunityLabel, graph);
|
|
126
126
|
// Generate kebab name
|
|
@@ -392,13 +392,18 @@ const gatherEntryPoints = (members) => {
|
|
|
392
392
|
/**
|
|
393
393
|
* @brief Gather execution flows touching this community
|
|
394
394
|
* @param {string[]} rawIds - Raw community IDs for this aggregated community
|
|
395
|
+
* @param {MemberSymbol[]} members - Member symbols, including raw singleton assignments
|
|
395
396
|
* @param {ProcessNode[]} processes - All detected processes
|
|
396
|
-
* @returns {ProcessNode[]} Processes
|
|
397
|
+
* @returns {ProcessNode[]} Processes matching the community IDs or member symbols, sorted by stepCount
|
|
397
398
|
*/
|
|
398
|
-
const gatherFlows = (rawIds, processes) => {
|
|
399
|
+
const gatherFlows = (rawIds, members, processes) => {
|
|
399
400
|
const rawIdSet = new Set(rawIds);
|
|
401
|
+
const memberIds = new Set(members.map((member) => member.id));
|
|
400
402
|
return processes
|
|
401
|
-
.filter((proc) => proc.communities.some((cid) => rawIdSet.has(cid))
|
|
403
|
+
.filter((proc) => proc.communities.some((cid) => rawIdSet.has(cid)) ||
|
|
404
|
+
// Filtered singleton communities are absent from process metadata,
|
|
405
|
+
// but their symbols still participate in detected execution traces.
|
|
406
|
+
proc.trace.some((nodeId) => memberIds.has(nodeId)))
|
|
402
407
|
.sort((a, b) => b.stepCount - a.stepCount);
|
|
403
408
|
};
|
|
404
409
|
/**
|
|
@@ -30,6 +30,7 @@ export declare const createWatchIgnorePredicate: (repoPath: string, options?: Ig
|
|
|
30
30
|
/**
|
|
31
31
|
* Create a glob-compatible ignore filter combining:
|
|
32
32
|
* - .gitignore / .gitnexusignore patterns (via `ignore` package)
|
|
33
|
+
* - nested .gitignore files, scoped to their own directory (#2675)
|
|
33
34
|
* - Hardcoded DEFAULT_IGNORE_LIST, IGNORED_EXTENSIONS, IGNORED_FILES
|
|
34
35
|
*
|
|
35
36
|
* Returns an IgnoreLike object for glob's `ignore` option,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ignore from 'ignore';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, openSync, readFileSync, } from 'fs';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import nodePath from 'path';
|
|
5
5
|
import { readRepoControlFile } from './repo-control-file.js';
|
|
@@ -472,9 +472,152 @@ const hasExplicitUnignore = (ig, rel) => {
|
|
|
472
472
|
}
|
|
473
473
|
return false;
|
|
474
474
|
};
|
|
475
|
+
/**
|
|
476
|
+
* Read a nested `.gitignore` only if it is a regular file, not a symlink.
|
|
477
|
+
*
|
|
478
|
+
* git does not follow a symlinked `.gitignore` in the working tree, and
|
|
479
|
+
* reading one could pull rules from outside the repository. Where the
|
|
480
|
+
* platform supports it, the file is opened with O_NOFOLLOW (a symlink fails
|
|
481
|
+
* with ELOOP). Windows has no O_NOFOLLOW, so there the path is lstat'ed after
|
|
482
|
+
* opening and must be the same regular file as the open descriptor. Either
|
|
483
|
+
* way the content is read through the descriptor that was checked, never by
|
|
484
|
+
* path, so the file cannot be swapped between the check and the read.
|
|
485
|
+
*
|
|
486
|
+
* The open also passes O_NONBLOCK where it exists. Opening a FIFO for reading
|
|
487
|
+
* blocks in open(2) until a writer appears, so a `.gitignore` that is a FIFO
|
|
488
|
+
* would hang the scan before the isFile() check could reject it (glob's
|
|
489
|
+
* ignore callback is synchronous). The flag makes that open return at once
|
|
490
|
+
* and changes nothing for a regular file. Same reasoning as readBoundedFile
|
|
491
|
+
* in src/core/ingestion/asyncapi/document.ts.
|
|
492
|
+
*/
|
|
493
|
+
const readNestedGitignore = (filePath) => {
|
|
494
|
+
const noFollow = fsConstants.O_NOFOLLOW;
|
|
495
|
+
const nonBlock = fsConstants.O_NONBLOCK;
|
|
496
|
+
const fd = openSync(filePath, fsConstants.O_RDONLY | (noFollow ?? 0) | (nonBlock ?? 0));
|
|
497
|
+
try {
|
|
498
|
+
const stat = fstatSync(fd);
|
|
499
|
+
if (!stat.isFile())
|
|
500
|
+
return null;
|
|
501
|
+
if (noFollow === undefined) {
|
|
502
|
+
const link = lstatSync(filePath);
|
|
503
|
+
if (!link.isFile() || link.ino !== stat.ino || link.dev !== stat.dev)
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
return readFileSync(fd, 'utf-8');
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
closeSync(fd);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Resolve `.gitignore` files below the repository root (#2675).
|
|
514
|
+
*
|
|
515
|
+
* `loadIgnoreRules` only reads the root `.gitignore`, so a monorepo package
|
|
516
|
+
* or checked-out submodule with its own `.gitignore` had its generated
|
|
517
|
+
* output indexed anyway. Each nested file is read lazily (glob's filter is
|
|
518
|
+
* synchronous) and cached per directory, and its patterns are matched
|
|
519
|
+
* against the path relative to that directory, like git does.
|
|
520
|
+
*
|
|
521
|
+
* Returns the effective decision when nested rules affect the path or an
|
|
522
|
+
* ancestor, and `undefined` otherwise. Root rules participate so a directory
|
|
523
|
+
* negation does not erase independent root exclusions for its children.
|
|
524
|
+
* The caller still gives `.gitnexusignore` its higher precedence.
|
|
525
|
+
*/
|
|
526
|
+
const createNestedGitignoreMatcher = (repoPath, rootRules) => {
|
|
527
|
+
const rulesFor = (dirRel) => {
|
|
528
|
+
let rules = null;
|
|
529
|
+
const filePath = nodePath.join(repoPath, dirRel, '.gitignore');
|
|
530
|
+
try {
|
|
531
|
+
const content = readNestedGitignore(filePath);
|
|
532
|
+
if (content !== null)
|
|
533
|
+
rules = ignore().add(content);
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
const code = err.code;
|
|
537
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR' && code !== 'ELOOP') {
|
|
538
|
+
logger.warn(` Warning: could not read ${filePath}: ${err.message}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return rules;
|
|
542
|
+
};
|
|
543
|
+
const relativeTo = (base, rel) => base ? rel.slice(base.length + 1) : rel;
|
|
544
|
+
const match = (scopes, rel, isDirectory) => {
|
|
545
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
546
|
+
const { base, rules } = scopes[i];
|
|
547
|
+
const sub = relativeTo(base, rel);
|
|
548
|
+
const result = rules.test(isDirectory ? `${sub}/` : sub);
|
|
549
|
+
if (result.ignored || result.unignored) {
|
|
550
|
+
return { ignored: result.ignored, nested: base !== '' };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
};
|
|
555
|
+
const contexts = new Map([
|
|
556
|
+
[
|
|
557
|
+
'',
|
|
558
|
+
{
|
|
559
|
+
scopes: rootRules ? [{ base: '', rules: rootRules }] : [],
|
|
560
|
+
ignored: false,
|
|
561
|
+
nested: false,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
]);
|
|
565
|
+
const contextFor = (dir) => {
|
|
566
|
+
const cached = contexts.get(dir);
|
|
567
|
+
if (cached)
|
|
568
|
+
return cached;
|
|
569
|
+
const parentDir = nodePath.posix.dirname(dir);
|
|
570
|
+
const parent = contextFor(parentDir === '.' ? '' : parentDir);
|
|
571
|
+
// A .gitignore inside an excluded directory cannot bring that directory
|
|
572
|
+
// back. Do not read rules below a parent that traversal would prune.
|
|
573
|
+
if (parent.ignored) {
|
|
574
|
+
contexts.set(dir, parent);
|
|
575
|
+
return parent;
|
|
576
|
+
}
|
|
577
|
+
const result = match(parent.scopes, dir, true);
|
|
578
|
+
const context = {
|
|
579
|
+
scopes: parent.scopes,
|
|
580
|
+
ignored: result?.ignored ?? false,
|
|
581
|
+
nested: parent.nested || (result?.nested ?? false),
|
|
582
|
+
};
|
|
583
|
+
if (!context.ignored) {
|
|
584
|
+
context.scopes = parent.scopes.map(({ base, rules }) => {
|
|
585
|
+
const sub = relativeTo(base, dir);
|
|
586
|
+
if (!rules.test(`${sub}/`).ignored)
|
|
587
|
+
return { base, rules };
|
|
588
|
+
// A deeper rule let us enter this directory. Clear only its inherited
|
|
589
|
+
// exclusion in the shallower layer; child rules must still be tested.
|
|
590
|
+
// Keep patterns in their original scope, and escape this literal path.
|
|
591
|
+
const literal = sub.replace(/[\\*?\[\]]/g, '\\$&');
|
|
592
|
+
return {
|
|
593
|
+
base,
|
|
594
|
+
rules: ignore()
|
|
595
|
+
.add(rules)
|
|
596
|
+
.add({ pattern: `!/${literal}/` }),
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
const rules = rulesFor(dir);
|
|
600
|
+
if (rules)
|
|
601
|
+
context.scopes.push({ base: dir, rules });
|
|
602
|
+
}
|
|
603
|
+
contexts.set(dir, context);
|
|
604
|
+
return context;
|
|
605
|
+
};
|
|
606
|
+
return (rel, isDirectory) => {
|
|
607
|
+
const parentDir = nodePath.posix.dirname(rel);
|
|
608
|
+
const parent = contextFor(parentDir === '.' ? '' : parentDir);
|
|
609
|
+
if (parent.ignored)
|
|
610
|
+
return parent.nested ? true : undefined;
|
|
611
|
+
const result = match(parent.scopes, rel, isDirectory);
|
|
612
|
+
if (parent.nested || result?.nested)
|
|
613
|
+
return result?.ignored ?? false;
|
|
614
|
+
return undefined;
|
|
615
|
+
};
|
|
616
|
+
};
|
|
475
617
|
/**
|
|
476
618
|
* Create a glob-compatible ignore filter combining:
|
|
477
619
|
* - .gitignore / .gitnexusignore patterns (via `ignore` package)
|
|
620
|
+
* - nested .gitignore files, scoped to their own directory (#2675)
|
|
478
621
|
* - Hardcoded DEFAULT_IGNORE_LIST, IGNORED_EXTENSIONS, IGNORED_FILES
|
|
479
622
|
*
|
|
480
623
|
* Returns an IgnoreLike object for glob's `ignore` option,
|
|
@@ -491,6 +634,13 @@ const hasExplicitUnignore = (ig, rel) => {
|
|
|
491
634
|
*/
|
|
492
635
|
export const createIgnoreFilter = async (repoPath, options) => {
|
|
493
636
|
const ig = await loadIgnoreRules(repoPath, options);
|
|
637
|
+
const skipGitignore = options?.noGitignore ?? !!process.env.GITNEXUS_NO_GITIGNORE;
|
|
638
|
+
const nestedIgnores = skipGitignore ? null : createNestedGitignoreMatcher(repoPath, ig);
|
|
639
|
+
// A nested negation outranks the root .gitignore, as in git, but not the
|
|
640
|
+
// user's .gitnexusignore, so keep a matcher for that file on its own.
|
|
641
|
+
const nexusIgnore = nestedIgnores
|
|
642
|
+
? await loadIgnoreRules(repoPath, { ...options, noGitignore: true, noGlobalIgnore: true })
|
|
643
|
+
: null;
|
|
494
644
|
return {
|
|
495
645
|
ignored(p) {
|
|
496
646
|
// The `ignore` package expects POSIX separators; path-scurry can surface
|
|
@@ -498,6 +648,27 @@ export const createIgnoreFilter = async (repoPath, options) => {
|
|
|
498
648
|
const rel = p.relative().replace(/\\/g, '/');
|
|
499
649
|
if (!rel)
|
|
500
650
|
return false;
|
|
651
|
+
// Nested .gitignore files below the root (#2675). .gitnexusignore
|
|
652
|
+
// comes first, then the deepest nested .gitignore, which outranks the
|
|
653
|
+
// root .gitignore as in git. The nested matcher preserves independent
|
|
654
|
+
// root exclusions; a nested negation never rescues a hardcoded default.
|
|
655
|
+
// With no nested opinion the original order below applies unchanged.
|
|
656
|
+
if (nestedIgnores) {
|
|
657
|
+
if (nexusIgnore) {
|
|
658
|
+
if (hasExplicitUnignore(nexusIgnore, rel) && !ig?.ignores(rel))
|
|
659
|
+
return false;
|
|
660
|
+
if (nexusIgnore.ignores(rel))
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
const nested = nestedIgnores(rel, false);
|
|
664
|
+
if (nested === true)
|
|
665
|
+
return true;
|
|
666
|
+
if (nested === false) {
|
|
667
|
+
if (ig && hasExplicitUnignore(ig, rel) && !ig.ignores(rel))
|
|
668
|
+
return false;
|
|
669
|
+
return shouldIgnorePath(rel);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
501
672
|
// User's .gitnexusignore negation takes precedence over hardcoded
|
|
502
673
|
// rules (#771). If any ancestor or the path itself was explicitly
|
|
503
674
|
// unignored AND no more-specific rule re-ignores this exact path,
|
|
@@ -519,6 +690,25 @@ export const createIgnoreFilter = async (repoPath, options) => {
|
|
|
519
690
|
// list check below is defense-in-depth — do not remove `dot: false`
|
|
520
691
|
// assuming this covers it.
|
|
521
692
|
const rel = p.relative().replace(/\\/g, '/');
|
|
693
|
+
// Nested .gitignore files below the root (#2675), same precedence as in
|
|
694
|
+
// `ignored` above.
|
|
695
|
+
if (nestedIgnores && rel) {
|
|
696
|
+
if (nexusIgnore) {
|
|
697
|
+
if (hasExplicitUnignore(nexusIgnore, rel) && !ig?.ignores(rel + '/')) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
if (nexusIgnore.ignores(rel + '/'))
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
const nested = nestedIgnores(rel, true);
|
|
704
|
+
if (nested === true)
|
|
705
|
+
return true;
|
|
706
|
+
if (nested === false) {
|
|
707
|
+
if (ig && hasExplicitUnignore(ig, rel) && !ig.ignores(rel + '/'))
|
|
708
|
+
return false;
|
|
709
|
+
return isHardcodedIgnoredDirectoryAtPath(repoPath, nodePath.join(repoPath, rel));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
522
712
|
// User's .gitnexusignore negation takes precedence (#771) — if the
|
|
523
713
|
// user explicitly unignored this directory or any ancestor via a
|
|
524
714
|
// !pattern rule, allow descent even if the directory name is in
|
|
@@ -62,7 +62,10 @@ export interface CommunityMembership {
|
|
|
62
62
|
}
|
|
63
63
|
export interface CommunityDetectionResult {
|
|
64
64
|
communities: CommunityNode[];
|
|
65
|
+
/** Assignments to retained communities, safe to emit as MEMBER_OF edges. */
|
|
65
66
|
memberships: CommunityMembership[];
|
|
67
|
+
/** All Leiden assignments, including filtered singletons. Optional for legacy producers. */
|
|
68
|
+
rawMemberships?: CommunityMembership[];
|
|
66
69
|
stats: {
|
|
67
70
|
totalCommunities: number;
|
|
68
71
|
modularity: number;
|
|
@@ -107,6 +107,7 @@ export const processCommunities = async (knowledgeGraph, onProgress, options = {
|
|
|
107
107
|
return {
|
|
108
108
|
communities: [],
|
|
109
109
|
memberships: [],
|
|
110
|
+
rawMemberships: [],
|
|
110
111
|
stats: {
|
|
111
112
|
totalCommunities: 0,
|
|
112
113
|
modularity: 0,
|
|
@@ -124,18 +125,23 @@ export const processCommunities = async (knowledgeGraph, onProgress, options = {
|
|
|
124
125
|
// Step 3: Create community nodes with heuristic labels
|
|
125
126
|
const communityNodes = createCommunityNodes(details.communities, details.count, graph, knowledgeGraph);
|
|
126
127
|
onProgress?.('Creating membership edges...', 80);
|
|
127
|
-
// Step 4:
|
|
128
|
+
// Step 4: Preserve all assignments for skill generation, but only emit
|
|
129
|
+
// memberships for retained communities with a corresponding graph node.
|
|
130
|
+
const retainedCommunityIds = new Set(communityNodes.map((community) => community.id));
|
|
128
131
|
const memberships = [];
|
|
132
|
+
const rawMemberships = [];
|
|
129
133
|
Object.entries(details.communities).forEach(([nodeId, communityNum]) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
const communityId = `comm_${communityNum}`;
|
|
135
|
+
const membership = { nodeId, communityId };
|
|
136
|
+
rawMemberships.push(membership);
|
|
137
|
+
if (retainedCommunityIds.has(communityId))
|
|
138
|
+
memberships.push(membership);
|
|
134
139
|
});
|
|
135
140
|
onProgress?.('Community detection complete!', 100);
|
|
136
141
|
return {
|
|
137
142
|
communities: communityNodes,
|
|
138
143
|
memberships,
|
|
144
|
+
rawMemberships,
|
|
139
145
|
stats: {
|
|
140
146
|
totalCommunities: details.count,
|
|
141
147
|
modularity: details.modularity,
|
package/package.json
CHANGED