gitnexus 1.6.3-rc.31 → 1.6.3-rc.33
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/core/graph/graph.js +115 -20
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/group/config-parser.d.ts +4 -0
- package/dist/core/group/config-parser.js +18 -1
- package/dist/core/group/cross-impact.js +4 -6
- package/dist/core/group/service.js +45 -10
- package/dist/core/ingestion/ast-cache.d.ts +16 -1
- package/dist/core/ingestion/ast-cache.js +14 -2
- package/dist/core/ingestion/call-processor.js +9 -0
- package/dist/core/ingestion/import-processor.js +4 -0
- package/dist/core/ingestion/import-resolvers/python.js +9 -6
- package/dist/core/ingestion/language-provider.d.ts +12 -25
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +106 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +17 -0
- package/dist/core/ingestion/languages/python/import-target.js +95 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +14 -0
- package/dist/core/ingestion/languages/python/query.js +272 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
- package/dist/core/ingestion/languages/python.js +14 -0
- package/dist/core/ingestion/mro-processor.js +38 -22
- package/dist/core/ingestion/parsing-processor.d.ts +9 -1
- package/dist/core/ingestion/parsing-processor.js +25 -3
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.d.ts +32 -5
- package/dist/core/ingestion/registry-primary-flag.js +38 -6
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +1 -1
- package/dist/core/ingestion/scope-extractor-bridge.js +2 -2
- package/dist/core/ingestion/scope-extractor.d.ts +1 -2
- package/dist/core/ingestion/scope-extractor.js +151 -16
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +168 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +75 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +72 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +56 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +101 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +101 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +32 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +137 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +25 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +61 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +200 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +123 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +55 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +131 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +100 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +287 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +109 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
- package/dist/core/ingestion/utils/ast-helpers.js +70 -0
- package/package.json +3 -3
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
package/dist/core/graph/graph.js
CHANGED
|
@@ -1,28 +1,113 @@
|
|
|
1
|
+
/** Fresh empty iterator per call — `[].values()` returns a new
|
|
2
|
+
* exhausted iterator each invocation, so empty-type lookups don't
|
|
3
|
+
* share a single already-exhausted iterator across callers. */
|
|
4
|
+
function emptyRelIter() {
|
|
5
|
+
return [].values();
|
|
6
|
+
}
|
|
1
7
|
export const createKnowledgeGraph = () => {
|
|
2
8
|
const nodeMap = new Map();
|
|
3
9
|
const relationshipMap = new Map();
|
|
10
|
+
// Per-type index maintained alongside `relationshipMap`. Bucket
|
|
11
|
+
// values are `Map<id, Relationship>` so per-type iteration is cheap
|
|
12
|
+
// and per-edge removal is O(1). See plan
|
|
13
|
+
// docs/plans/2026-04-20-002-perf-parse-heritage-mro-plan.md (Unit 1).
|
|
14
|
+
const relationshipsByType = new Map();
|
|
15
|
+
// Reverse-adjacency index: nodeId → Set<relId> of every edge where
|
|
16
|
+
// this node appears as source OR target. Maintained on writeRel /
|
|
17
|
+
// deleteRel so `removeNode` can delete a node's edges in
|
|
18
|
+
// O(edges-touching-node) instead of O(total-edges).
|
|
19
|
+
const edgeIdsByNode = new Map();
|
|
20
|
+
// File index: filePath → Set<nodeId>. Maintained on addNode /
|
|
21
|
+
// removeNode so `removeNodesByFile` reaches its file's nodes
|
|
22
|
+
// directly instead of scanning the whole node map.
|
|
23
|
+
const nodeIdsByFile = new Map();
|
|
24
|
+
// Private helpers that encode the dual-index invariants in one
|
|
25
|
+
// place. All mutation paths go through these — adding a new
|
|
26
|
+
// mutation method only needs to call the helper, not remember to
|
|
27
|
+
// touch every index.
|
|
28
|
+
const addToBucket = (map, key, value) => {
|
|
29
|
+
let bucket = map.get(key);
|
|
30
|
+
if (bucket === undefined) {
|
|
31
|
+
bucket = new Set();
|
|
32
|
+
map.set(key, bucket);
|
|
33
|
+
}
|
|
34
|
+
bucket.add(value);
|
|
35
|
+
};
|
|
36
|
+
const removeFromBucket = (map, key, value) => {
|
|
37
|
+
const bucket = map.get(key);
|
|
38
|
+
if (bucket === undefined)
|
|
39
|
+
return;
|
|
40
|
+
bucket.delete(value);
|
|
41
|
+
if (bucket.size === 0)
|
|
42
|
+
map.delete(key);
|
|
43
|
+
};
|
|
44
|
+
const writeRel = (rel) => {
|
|
45
|
+
relationshipMap.set(rel.id, rel);
|
|
46
|
+
let typeBucket = relationshipsByType.get(rel.type);
|
|
47
|
+
if (typeBucket === undefined) {
|
|
48
|
+
typeBucket = new Map();
|
|
49
|
+
relationshipsByType.set(rel.type, typeBucket);
|
|
50
|
+
}
|
|
51
|
+
typeBucket.set(rel.id, rel);
|
|
52
|
+
addToBucket(edgeIdsByNode, rel.sourceId, rel.id);
|
|
53
|
+
// Guard against a self-edge writing the same rel.id into the
|
|
54
|
+
// same Set twice — Set dedup handles it, but we skip explicitly
|
|
55
|
+
// for clarity.
|
|
56
|
+
if (rel.targetId !== rel.sourceId) {
|
|
57
|
+
addToBucket(edgeIdsByNode, rel.targetId, rel.id);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const deleteRel = (rel) => {
|
|
61
|
+
relationshipMap.delete(rel.id);
|
|
62
|
+
const typeBucket = relationshipsByType.get(rel.type);
|
|
63
|
+
if (typeBucket !== undefined) {
|
|
64
|
+
typeBucket.delete(rel.id);
|
|
65
|
+
if (typeBucket.size === 0)
|
|
66
|
+
relationshipsByType.delete(rel.type);
|
|
67
|
+
}
|
|
68
|
+
removeFromBucket(edgeIdsByNode, rel.sourceId, rel.id);
|
|
69
|
+
if (rel.targetId !== rel.sourceId) {
|
|
70
|
+
removeFromBucket(edgeIdsByNode, rel.targetId, rel.id);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
4
73
|
const addNode = (node) => {
|
|
5
|
-
if (
|
|
6
|
-
|
|
74
|
+
if (nodeMap.has(node.id))
|
|
75
|
+
return;
|
|
76
|
+
nodeMap.set(node.id, node);
|
|
77
|
+
const filePath = node.properties?.filePath;
|
|
78
|
+
if (typeof filePath === 'string' && filePath.length > 0) {
|
|
79
|
+
addToBucket(nodeIdsByFile, filePath, node.id);
|
|
7
80
|
}
|
|
8
81
|
};
|
|
9
82
|
const addRelationship = (relationship) => {
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
83
|
+
if (relationshipMap.has(relationship.id))
|
|
84
|
+
return;
|
|
85
|
+
writeRel(relationship);
|
|
13
86
|
};
|
|
14
87
|
/**
|
|
15
|
-
* Remove a single node and all relationships involving it
|
|
88
|
+
* Remove a single node and all relationships involving it.
|
|
89
|
+
* O(edges-touching-node) via the reverse-adjacency index — no full
|
|
90
|
+
* relationshipMap scan.
|
|
16
91
|
*/
|
|
17
92
|
const removeNode = (nodeId) => {
|
|
18
|
-
|
|
93
|
+
const node = nodeMap.get(nodeId);
|
|
94
|
+
if (node === undefined)
|
|
19
95
|
return false;
|
|
20
96
|
nodeMap.delete(nodeId);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
97
|
+
const filePath = node.properties?.filePath;
|
|
98
|
+
if (typeof filePath === 'string' && filePath.length > 0) {
|
|
99
|
+
removeFromBucket(nodeIdsByFile, filePath, nodeId);
|
|
100
|
+
}
|
|
101
|
+
const touchingEdgeIds = edgeIdsByNode.get(nodeId);
|
|
102
|
+
if (touchingEdgeIds !== undefined) {
|
|
103
|
+
// Snapshot the ids before iterating — deleteRel mutates the same
|
|
104
|
+
// Set via removeFromBucket, which would break mid-loop iteration.
|
|
105
|
+
for (const relId of [...touchingEdgeIds]) {
|
|
106
|
+
const rel = relationshipMap.get(relId);
|
|
107
|
+
if (rel !== undefined)
|
|
108
|
+
deleteRel(rel);
|
|
25
109
|
}
|
|
110
|
+
edgeIdsByNode.delete(nodeId);
|
|
26
111
|
}
|
|
27
112
|
return true;
|
|
28
113
|
};
|
|
@@ -31,20 +116,26 @@ export const createKnowledgeGraph = () => {
|
|
|
31
116
|
* Returns true if the relationship existed and was removed, false otherwise.
|
|
32
117
|
*/
|
|
33
118
|
const removeRelationship = (relationshipId) => {
|
|
34
|
-
|
|
119
|
+
const rel = relationshipMap.get(relationshipId);
|
|
120
|
+
if (rel === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
deleteRel(rel);
|
|
123
|
+
return true;
|
|
35
124
|
};
|
|
36
125
|
/**
|
|
37
126
|
* Remove all nodes (and their relationships) belonging to a file.
|
|
127
|
+
* O(file-nodes × avg-edges-per-node) via the file index — no full
|
|
128
|
+
* node-map scan.
|
|
38
129
|
*/
|
|
39
130
|
const removeNodesByFile = (filePath) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
131
|
+
const nodeIds = nodeIdsByFile.get(filePath);
|
|
132
|
+
if (nodeIds === undefined)
|
|
133
|
+
return 0;
|
|
134
|
+
// Snapshot before iterating — removeNode mutates nodeIdsByFile.
|
|
135
|
+
const snapshot = [...nodeIds];
|
|
136
|
+
for (const nodeId of snapshot)
|
|
137
|
+
removeNode(nodeId);
|
|
138
|
+
return snapshot.length;
|
|
48
139
|
};
|
|
49
140
|
return {
|
|
50
141
|
get nodes() {
|
|
@@ -55,6 +146,10 @@ export const createKnowledgeGraph = () => {
|
|
|
55
146
|
},
|
|
56
147
|
iterNodes: () => nodeMap.values(),
|
|
57
148
|
iterRelationships: () => relationshipMap.values(),
|
|
149
|
+
iterRelationshipsByType: (type) => {
|
|
150
|
+
const bucket = relationshipsByType.get(type);
|
|
151
|
+
return bucket === undefined ? emptyRelIter() : bucket.values();
|
|
152
|
+
},
|
|
58
153
|
forEachNode(fn) {
|
|
59
154
|
nodeMap.forEach(fn);
|
|
60
155
|
},
|
|
@@ -6,12 +6,23 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This file only defines the CLI's KnowledgeGraph with mutation methods.
|
|
8
8
|
*/
|
|
9
|
-
import type { GraphNode, GraphRelationship } from '../../_shared/index.js';
|
|
9
|
+
import type { GraphNode, GraphRelationship, RelationshipType } from '../../_shared/index.js';
|
|
10
10
|
export interface KnowledgeGraph {
|
|
11
11
|
nodes: GraphNode[];
|
|
12
12
|
relationships: GraphRelationship[];
|
|
13
13
|
iterNodes: () => IterableIterator<GraphNode>;
|
|
14
14
|
iterRelationships: () => IterableIterator<GraphRelationship>;
|
|
15
|
+
/**
|
|
16
|
+
* Iterate ONLY relationships of the given type, backed by a per-type
|
|
17
|
+
* index maintained in `addRelationship` / `removeRelationship` /
|
|
18
|
+
* `removeNode` / `removeNodesByFile`. Returns an empty iterator when
|
|
19
|
+
* the graph contains no relationships of that type.
|
|
20
|
+
*
|
|
21
|
+
* Prefer this over `iterRelationships()` + per-edge type filtering
|
|
22
|
+
* for hot paths (MRO setup, heritage walks). Backwards-compatible:
|
|
23
|
+
* existing `iterRelationships()` callers keep working.
|
|
24
|
+
*/
|
|
25
|
+
iterRelationshipsByType: (type: RelationshipType) => IterableIterator<GraphRelationship>;
|
|
15
26
|
forEachNode: (fn: (node: GraphNode) => void) => void;
|
|
16
27
|
forEachRelationship: (fn: (rel: GraphRelationship) => void) => void;
|
|
17
28
|
getNode: (id: string) => GraphNode | undefined;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import type { GroupConfig } from './types.js';
|
|
2
2
|
export declare function parseGroupConfig(yamlContent: string): GroupConfig;
|
|
3
|
+
export declare class GroupNotFoundError extends Error {
|
|
4
|
+
readonly groupName: string;
|
|
5
|
+
constructor(groupName: string);
|
|
6
|
+
}
|
|
3
7
|
export declare function loadGroupConfig(groupDir: string): Promise<GroupConfig>;
|
|
@@ -74,10 +74,27 @@ export function parseGroupConfig(yamlContent) {
|
|
|
74
74
|
matching,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
export class GroupNotFoundError extends Error {
|
|
78
|
+
groupName;
|
|
79
|
+
constructor(groupName) {
|
|
80
|
+
super(`Group "${groupName}" not found`);
|
|
81
|
+
this.groupName = groupName;
|
|
82
|
+
this.name = 'GroupNotFoundError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
77
85
|
export async function loadGroupConfig(groupDir) {
|
|
78
86
|
const fsp = await import('node:fs/promises');
|
|
79
87
|
const path = await import('node:path');
|
|
80
88
|
const yamlPath = path.join(groupDir, 'group.yaml');
|
|
81
|
-
|
|
89
|
+
let content;
|
|
90
|
+
try {
|
|
91
|
+
content = await fsp.readFile(yamlPath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code === 'ENOENT') {
|
|
95
|
+
throw new GroupNotFoundError(path.basename(groupDir));
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
82
99
|
return parseGroupConfig(content);
|
|
83
100
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fsp from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { loadGroupConfig } from './config-parser.js';
|
|
7
|
+
import { GroupNotFoundError, loadGroupConfig } from './config-parser.js';
|
|
8
8
|
import { fileMatchesServicePrefix, normalizeServicePrefix, repoInSubgroup, } from './group-path-utils.js';
|
|
9
9
|
import { getGroupDir } from './storage.js';
|
|
10
10
|
import { closeBridgeDb, openBridgeDbReadOnly, queryBridge, readBridgeMeta } from './bridge-db.js';
|
|
@@ -242,6 +242,8 @@ export async function runGroupImpact(deps, params) {
|
|
|
242
242
|
config = await loadGroupConfig(groupDir);
|
|
243
243
|
}
|
|
244
244
|
catch (e) {
|
|
245
|
+
if (e instanceof GroupNotFoundError)
|
|
246
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
245
247
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
246
248
|
}
|
|
247
249
|
const resolved = await resolveGroupRepo(deps.port, config, repoPath);
|
|
@@ -255,13 +257,10 @@ export async function runGroupImpact(deps, params) {
|
|
|
255
257
|
includeTests,
|
|
256
258
|
minConfidence,
|
|
257
259
|
};
|
|
258
|
-
// Single shared deadline for Phase 1 (local walk) + Phase 2 (bridge fan-out).
|
|
259
|
-
// Phase 1 still gets the full budget; Phase 2 only uses whatever wall-clock
|
|
260
|
-
// time is left, so total work cannot exceed `timeoutMs`.
|
|
261
260
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
262
261
|
const { value: local, timedOut: localTimedOut } = await safeLocalImpact(deps.port, resolved, impactParams, timeoutMs);
|
|
263
262
|
if (localTimedOut) {
|
|
264
|
-
const
|
|
263
|
+
const _base = local;
|
|
265
264
|
return {
|
|
266
265
|
local,
|
|
267
266
|
group: name,
|
|
@@ -361,7 +360,6 @@ export async function runGroupImpact(deps, params) {
|
|
|
361
360
|
continue;
|
|
362
361
|
}
|
|
363
362
|
if (!repoInSubgroup(n.neighborRepo, subgroup)) {
|
|
364
|
-
// CrossLink convention: consumer -> provider
|
|
365
363
|
outOfScope.push({
|
|
366
364
|
from: direction === 'upstream' ? n.neighborRepo : repoPath,
|
|
367
365
|
to: direction === 'upstream' ? repoPath : n.neighborRepo,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fsp from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { checkStaleness } from '../git-staleness.js';
|
|
8
|
-
import { loadGroupConfig } from './config-parser.js';
|
|
8
|
+
import { GroupNotFoundError, loadGroupConfig } from './config-parser.js';
|
|
9
9
|
import { fileMatchesServicePrefix, normalizeServicePrefix, repoInSubgroup, } from './group-path-utils.js';
|
|
10
10
|
import { getDefaultGitnexusDir, getGroupDir, listGroups, readContractRegistry } from './storage.js';
|
|
11
11
|
import { syncGroup } from './sync.js';
|
|
@@ -134,7 +134,15 @@ export class GroupService {
|
|
|
134
134
|
return { groups };
|
|
135
135
|
}
|
|
136
136
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
137
|
-
|
|
137
|
+
let config;
|
|
138
|
+
try {
|
|
139
|
+
config = await loadGroupConfig(groupDir);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err instanceof GroupNotFoundError)
|
|
143
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
138
146
|
return {
|
|
139
147
|
name: config.name,
|
|
140
148
|
description: config.description,
|
|
@@ -147,7 +155,15 @@ export class GroupService {
|
|
|
147
155
|
if (!name)
|
|
148
156
|
return { error: 'name is required' };
|
|
149
157
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
150
|
-
|
|
158
|
+
let config;
|
|
159
|
+
try {
|
|
160
|
+
config = await loadGroupConfig(groupDir);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
if (err instanceof GroupNotFoundError)
|
|
164
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
151
167
|
const result = await syncGroup(config, {
|
|
152
168
|
groupDir,
|
|
153
169
|
exactOnly: Boolean(params.exactOnly),
|
|
@@ -222,6 +238,14 @@ export class GroupService {
|
|
|
222
238
|
config = await loadGroupConfig(groupDir);
|
|
223
239
|
}
|
|
224
240
|
catch (e) {
|
|
241
|
+
if (e instanceof GroupNotFoundError)
|
|
242
|
+
return {
|
|
243
|
+
group: name,
|
|
244
|
+
target: target || uid,
|
|
245
|
+
service: servicePrefix,
|
|
246
|
+
error: `Group "${name}" not found. Run group_list to see configured groups.`,
|
|
247
|
+
results: [],
|
|
248
|
+
};
|
|
225
249
|
return {
|
|
226
250
|
group: name,
|
|
227
251
|
target: target || uid,
|
|
@@ -231,9 +255,6 @@ export class GroupService {
|
|
|
231
255
|
};
|
|
232
256
|
}
|
|
233
257
|
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
234
|
-
// Per-repo work is independent (each repo opens its own DB handle and the
|
|
235
|
-
// group-level result preserves repo iteration order via the indexed map).
|
|
236
|
-
// Errors are caught per repo so one slow/failed member does not block the rest.
|
|
237
258
|
const results = await Promise.all(memberEntries.map(async ([repoPath, registryName]) => {
|
|
238
259
|
try {
|
|
239
260
|
const repoObj = await this.port.resolveRepo(registryName);
|
|
@@ -282,10 +303,16 @@ export class GroupService {
|
|
|
282
303
|
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
283
304
|
const subgroupExact = params.subgroupExact === true;
|
|
284
305
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
285
|
-
|
|
306
|
+
let config;
|
|
307
|
+
try {
|
|
308
|
+
config = await loadGroupConfig(groupDir);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if (err instanceof GroupNotFoundError)
|
|
312
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
286
315
|
const memberEntries = Object.entries(config.repos).filter(([repoPath]) => repoInSubgroup(repoPath, subgroup, subgroupExact));
|
|
287
|
-
// Per-repo query is independent; run them concurrently and isolate
|
|
288
|
-
// failures so one slow/failed member does not block the rest.
|
|
289
316
|
const perRepo = await Promise.all(memberEntries.map(async ([repoPath, registryName]) => {
|
|
290
317
|
try {
|
|
291
318
|
const repoObj = await this.port.resolveRepo(registryName);
|
|
@@ -324,7 +351,15 @@ export class GroupService {
|
|
|
324
351
|
if (!name)
|
|
325
352
|
return { error: 'name is required' };
|
|
326
353
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
327
|
-
|
|
354
|
+
let config;
|
|
355
|
+
try {
|
|
356
|
+
config = await loadGroupConfig(groupDir);
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
if (err instanceof GroupNotFoundError)
|
|
360
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
328
363
|
const registry = await readContractRegistry(groupDir);
|
|
329
364
|
const repoStatuses = {};
|
|
330
365
|
for (const [repoPath, registryName] of Object.entries(config.repos)) {
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import Parser from 'tree-sitter';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Minimal structural shape consumers need when reading Trees back
|
|
4
|
+
* through a phase-dependency boundary. Declared here so phases that
|
|
5
|
+
* receive ASTCache via `getPhaseOutput<...>` don't hand-roll their
|
|
6
|
+
* own inline structural types that silently drift when ASTCache's
|
|
7
|
+
* contract changes.
|
|
8
|
+
*
|
|
9
|
+
* Typed as `unknown` at the Tree boundary because consumers on the
|
|
10
|
+
* other side of the phase-output map don't share tree-sitter's type
|
|
11
|
+
* graph (e.g. COBOL's standalone processor).
|
|
12
|
+
*/
|
|
13
|
+
export interface ASTCacheReader {
|
|
14
|
+
get(filePath: string): unknown;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
export interface ASTCache extends ASTCacheReader {
|
|
3
18
|
get: (filePath: string) => Parser.Tree | undefined;
|
|
4
19
|
set: (filePath: string, tree: Parser.Tree) => void;
|
|
5
20
|
clear: () => void;
|
|
@@ -7,8 +7,20 @@ export const createASTCache = (maxSize = 50) => {
|
|
|
7
7
|
max: effectiveMax,
|
|
8
8
|
dispose: (tree) => {
|
|
9
9
|
try {
|
|
10
|
-
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter
|
|
11
|
-
//
|
|
10
|
+
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter
|
|
11
|
+
// trees are GC-managed and .delete is absent (no-op here).
|
|
12
|
+
//
|
|
13
|
+
// Single-owner invariant (load-bearing under WASM): a given
|
|
14
|
+
// Parser.Tree reference must live in AT MOST ONE ASTCache
|
|
15
|
+
// that disposes. The parse-phase chunk-local cache clears
|
|
16
|
+
// between chunks; the cross-phase `scopeTreeCache` (also an
|
|
17
|
+
// ASTCache today) holds the same Tree by reference. Under
|
|
18
|
+
// native tree-sitter this is benign (dispose is a no-op).
|
|
19
|
+
// If/when GitNexus adopts web-tree-sitter for sequential
|
|
20
|
+
// parsing, the cross-phase cache must either (a) skip
|
|
21
|
+
// writing Trees that are already owned by a disposing cache,
|
|
22
|
+
// or (b) use tree.copy() per entry. Failing to pick one
|
|
23
|
+
// will hand freed memory to scope-resolution.
|
|
12
24
|
tree.delete?.();
|
|
13
25
|
}
|
|
14
26
|
catch (e) {
|
|
@@ -26,6 +26,7 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
|
|
|
26
26
|
import { getProvider } from './languages/index.js';
|
|
27
27
|
import { generateId } from '../../lib/utils.js';
|
|
28
28
|
import { getLanguageFromFilename, SupportedLanguages } from '../../_shared/index.js';
|
|
29
|
+
import { isRegistryPrimary } from './registry-primary-flag.js';
|
|
29
30
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
30
31
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
31
32
|
import { FUNCTION_NODE_TYPES, findEnclosingClassId, findEnclosingClassInfo, genericFuncName, inferFunctionLabel, } from './utils/ast-helpers.js';
|
|
@@ -594,6 +595,9 @@ importedRawReturnTypesMap, heritageMap, bindingAccumulator) => {
|
|
|
594
595
|
const language = getLanguageFromFilename(file.path);
|
|
595
596
|
if (!language)
|
|
596
597
|
continue;
|
|
598
|
+
// Registry-primary gate: scope-based phase owns CALLS for this lang.
|
|
599
|
+
if (isRegistryPrimary(language))
|
|
600
|
+
continue;
|
|
597
601
|
if (!isLanguageAvailable(language)) {
|
|
598
602
|
if (skippedByLang) {
|
|
599
603
|
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
|
@@ -2162,6 +2166,11 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
2162
2166
|
onProgress?.(filesProcessed, totalFiles);
|
|
2163
2167
|
await yieldToEventLoop();
|
|
2164
2168
|
}
|
|
2169
|
+
// Registry-primary gate: skip Python (etc.) entirely when the
|
|
2170
|
+
// scope-based phase owns CALLS for this language.
|
|
2171
|
+
const fileLanguage = getLanguageFromFilename(filePath);
|
|
2172
|
+
if (fileLanguage && isRegistryPrimary(fileLanguage))
|
|
2173
|
+
continue;
|
|
2165
2174
|
ctx.enableCache(filePath);
|
|
2166
2175
|
const widenCache = new Map();
|
|
2167
2176
|
const receiverMap = fileReceiverTypes.get(filePath);
|
|
@@ -9,6 +9,7 @@ import { getTreeSitterBufferSize } from './constants.js';
|
|
|
9
9
|
import { loadImportConfigs } from './language-config.js';
|
|
10
10
|
import { buildSuffixIndex } from './import-resolvers/utils.js';
|
|
11
11
|
import { isDev } from './utils/env.js';
|
|
12
|
+
import { isRegistryPrimary } from './registry-primary-flag.js';
|
|
12
13
|
/** Group files by provider (only those with implicit import wiring), then call each wirer
|
|
13
14
|
* with its own language's files. O(n) over files, O(1) per provider lookup. */
|
|
14
15
|
function wireImplicitImports(files, importMap, addImportEdge, projectConfig) {
|
|
@@ -64,6 +65,9 @@ export function preprocessImportPath(sourceText, importNode, provider) {
|
|
|
64
65
|
function createImportEdgeHelpers(graph, importMap) {
|
|
65
66
|
let totalImportsResolved = 0;
|
|
66
67
|
const addImportGraphEdge = (filePath, resolvedPath) => {
|
|
68
|
+
const language = getLanguageFromFilename(filePath);
|
|
69
|
+
if (language !== null && isRegistryPrimary(language))
|
|
70
|
+
return;
|
|
67
71
|
const sourceId = generateId('File', filePath);
|
|
68
72
|
const targetId = generateId('File', resolvedPath);
|
|
69
73
|
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
|
@@ -45,12 +45,15 @@ export function resolvePythonImportInternal(currentFile, importPath, allFiles) {
|
|
|
45
45
|
return null;
|
|
46
46
|
// Normalize for Windows backslashes
|
|
47
47
|
const importerDir = currentFile.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
// Proximity check — only applies when the importer lives in a subdirectory.
|
|
49
|
+
// Root-level importers (importerDir === '') skip straight to the ancestor
|
|
50
|
+
// walk below, which handles the root case correctly (prefix becomes '').
|
|
51
|
+
if (importerDir) {
|
|
52
|
+
if (allFiles.has(`${importerDir}/${pathLike}/__init__.py`))
|
|
53
|
+
return `${importerDir}/${pathLike}/__init__.py`;
|
|
54
|
+
if (allFiles.has(`${importerDir}/${pathLike}.py`))
|
|
55
|
+
return `${importerDir}/${pathLike}.py`;
|
|
56
|
+
}
|
|
54
57
|
// Ancestor directory walk — Python resolves bare imports against sys.path entries,
|
|
55
58
|
// which typically includes the project root and package directories. Walk up from the
|
|
56
59
|
// importer's directory to find the module in an ancestor, preferring the closest match.
|
|
@@ -250,7 +250,18 @@ interface LanguageProviderConfig {
|
|
|
250
250
|
*
|
|
251
251
|
* Default: undefined (language continues to use legacy DAG).
|
|
252
252
|
*/
|
|
253
|
-
readonly emitScopeCaptures?: (sourceText: string, filePath: string
|
|
253
|
+
readonly emitScopeCaptures?: (sourceText: string, filePath: string,
|
|
254
|
+
/**
|
|
255
|
+
* Optional pre-parsed tree-sitter Tree the caller has already
|
|
256
|
+
* produced (e.g. from the parse phase's AST cache). When supplied,
|
|
257
|
+
* the provider SHOULD skip its own `parser.parse(sourceText)` and
|
|
258
|
+
* run its capture query against the supplied tree directly. Typed
|
|
259
|
+
* as `unknown` here to avoid leaking the tree-sitter dependency
|
|
260
|
+
* into the provider contract — the provider casts at use site.
|
|
261
|
+
* Cache miss (parameter omitted or undefined) is always safe and
|
|
262
|
+
* MUST trigger a fresh parse.
|
|
263
|
+
*/
|
|
264
|
+
cachedTree?: unknown) => readonly CaptureMatch[];
|
|
254
265
|
/**
|
|
255
266
|
* Interpret a raw `@import.statement` capture group into a `ParsedImport`.
|
|
256
267
|
* The central finalize algorithm resolves `ParsedImport.targetRaw` to a
|
|
@@ -288,18 +299,6 @@ interface LanguageProviderConfig {
|
|
|
288
299
|
* suffix — `@scope.function` → `'Function'`, etc.).
|
|
289
300
|
*/
|
|
290
301
|
readonly resolveScopeKind?: (captures: CaptureMatch) => ScopeKind | null;
|
|
291
|
-
/**
|
|
292
|
-
* Should this scope capture materialize as a real `Scope` node? Return
|
|
293
|
-
* `false` to skip scope creation while still emitting declarations that
|
|
294
|
-
* would have gone inside (they attach to the enclosing real scope).
|
|
295
|
-
*
|
|
296
|
-
* Example: Python `if`/`for`/`while` bodies capture as `@scope.block` but
|
|
297
|
-
* Python has no block scope — hook returns `false` and child declarations
|
|
298
|
-
* lift to the enclosing function/module.
|
|
299
|
-
*
|
|
300
|
-
* Default: undefined (treated as `true` — always create).
|
|
301
|
-
*/
|
|
302
|
-
readonly shouldCreateScope?: (captures: CaptureMatch) => boolean;
|
|
303
302
|
/**
|
|
304
303
|
* Override where a declaration's name becomes visible. By default the name
|
|
305
304
|
* is bound in the innermost enclosing scope; return a different `ScopeId`
|
|
@@ -382,18 +381,6 @@ interface LanguageProviderConfig {
|
|
|
382
381
|
* else treats as `'free'`).
|
|
383
382
|
*/
|
|
384
383
|
readonly classifyCallForm?: (captures: CaptureMatch, enclosingScope: Scope) => 'free' | 'member' | 'constructor' | 'index';
|
|
385
|
-
/**
|
|
386
|
-
* Does a binding at this scope shadow bindings of the same name in outer
|
|
387
|
-
* scopes? Default: any binding shadows (standard lexical scoping). Return
|
|
388
|
-
* `false` for transparent-scope edge cases (Python `from x import *`
|
|
389
|
-
* contexts, JS `var` hoisting quirks, COBOL PARAGRAPH transparency).
|
|
390
|
-
*
|
|
391
|
-
* Consulted by `Registry.lookup` Step 1 and by `resolveTypeRef` for
|
|
392
|
-
* shadowing decisions during the lexical chain walk.
|
|
393
|
-
*
|
|
394
|
-
* Default: undefined (treated as `true` — any binding shadows).
|
|
395
|
-
*/
|
|
396
|
-
readonly shouldShadow?: (scope: Scope, bindings: readonly BindingRef[]) => boolean;
|
|
397
384
|
/**
|
|
398
385
|
* Is this callable definition compatible with the given call-site arity?
|
|
399
386
|
* Language-specific rules: Python `*args`/`**kwargs`/defaults, JS default
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract Python arity metadata from a `function_definition` tree-sitter
|
|
3
|
+
* node — parameter count, required count, and (where present) a type
|
|
4
|
+
* list that the existing `pythonArityCompatibility` hook reads.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the legacy `buildMethodProps` conversion so scope-extracted
|
|
7
|
+
* defs carry the same arity semantics as the parse-worker path:
|
|
8
|
+
* - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
|
|
9
|
+
* - Defaulted params contribute to `optionalCount`, flipping
|
|
10
|
+
* `requiredParameterCount = total − optionalCount`.
|
|
11
|
+
* - Variadic (`*args` / `**kwargs`) collapses `parameterCount` to
|
|
12
|
+
* `undefined`, which `pythonArityCompatibility` then treats as
|
|
13
|
+
* `'unknown'` — keeping the candidate in the registry's lookup set.
|
|
14
|
+
* - `parameterTypes` is populated only with real type text, matching
|
|
15
|
+
* legacy behavior.
|
|
16
|
+
*/
|
|
17
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
18
|
+
interface PythonArityMetadata {
|
|
19
|
+
readonly parameterCount: number | undefined;
|
|
20
|
+
readonly requiredParameterCount: number | undefined;
|
|
21
|
+
readonly parameterTypes: readonly string[] | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function computePythonArityMetadata(fnNode: SyntaxNode): PythonArityMetadata;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract Python arity metadata from a `function_definition` tree-sitter
|
|
3
|
+
* node — parameter count, required count, and (where present) a type
|
|
4
|
+
* list that the existing `pythonArityCompatibility` hook reads.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the legacy `buildMethodProps` conversion so scope-extracted
|
|
7
|
+
* defs carry the same arity semantics as the parse-worker path:
|
|
8
|
+
* - `self` / `cls` are stripped (consumed by `extractPythonParameters`).
|
|
9
|
+
* - Defaulted params contribute to `optionalCount`, flipping
|
|
10
|
+
* `requiredParameterCount = total − optionalCount`.
|
|
11
|
+
* - Variadic (`*args` / `**kwargs`) collapses `parameterCount` to
|
|
12
|
+
* `undefined`, which `pythonArityCompatibility` then treats as
|
|
13
|
+
* `'unknown'` — keeping the candidate in the registry's lookup set.
|
|
14
|
+
* - `parameterTypes` is populated only with real type text, matching
|
|
15
|
+
* legacy behavior.
|
|
16
|
+
*/
|
|
17
|
+
import { pythonMethodConfig } from '../../method-extractors/configs/python.js';
|
|
18
|
+
export function computePythonArityMetadata(fnNode) {
|
|
19
|
+
const params = pythonMethodConfig.extractParameters?.(fnNode) ?? [];
|
|
20
|
+
let hasVariadic = false;
|
|
21
|
+
let optionalCount = 0;
|
|
22
|
+
const types = [];
|
|
23
|
+
for (const p of params) {
|
|
24
|
+
if (p.isVariadic)
|
|
25
|
+
hasVariadic = true;
|
|
26
|
+
else if (p.isOptional)
|
|
27
|
+
optionalCount++;
|
|
28
|
+
if (p.type !== null)
|
|
29
|
+
types.push(p.type);
|
|
30
|
+
}
|
|
31
|
+
const total = params.length;
|
|
32
|
+
const parameterCount = hasVariadic ? undefined : total;
|
|
33
|
+
// Unlike legacy `buildMethodProps`, we populate `requiredParameterCount`
|
|
34
|
+
// whenever the function isn't variadic — even when it equals
|
|
35
|
+
// `parameterCount`. The scope-resolution registry needs a concrete min
|
|
36
|
+
// to rule out under-application (e.g. picking `write_audit(x, y)` for
|
|
37
|
+
// a 1-arg call). Legacy could get away with leaving it undefined
|
|
38
|
+
// because its call-graph builder had a separate arity pre-filter.
|
|
39
|
+
const requiredParameterCount = hasVariadic ? undefined : total - optionalCount;
|
|
40
|
+
return {
|
|
41
|
+
parameterCount,
|
|
42
|
+
requiredParameterCount,
|
|
43
|
+
parameterTypes: types.length > 0 ? types : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python arity check, accommodating `*args`, `**kwargs`, and defaults.
|
|
3
|
+
*
|
|
4
|
+
* The `def` metadata we care about (set by the existing Python method/
|
|
5
|
+
* function extractor):
|
|
6
|
+
* - `parameterCount` — total positional + keyword params
|
|
7
|
+
* - `requiredParameterCount` — min required (excludes defaults / `*args` / `**kwargs`)
|
|
8
|
+
* - `parameterTypes` — present when types are known; we also use it
|
|
9
|
+
* as a "we have varargs" hint (`'*args'`,
|
|
10
|
+
* `'**kwargs'` literals appear in the array).
|
|
11
|
+
*
|
|
12
|
+
* Verdicts:
|
|
13
|
+
* - `'compatible'` — `requiredParameterCount <= argCount <= parameterCount`,
|
|
14
|
+
* OR the def takes `*args` (then any `argCount >= required` ok).
|
|
15
|
+
* - `'incompatible'` — argCount is below required, OR above max with no `*args`.
|
|
16
|
+
* - `'unknown'` — def metadata is absent / incomplete.
|
|
17
|
+
*
|
|
18
|
+
* `'incompatible'` is a soft signal in `Registry.lookup` (penalized but
|
|
19
|
+
* still considered when no compatible candidate exists), per RFC §4.
|
|
20
|
+
*/
|
|
21
|
+
import type { Callsite, SymbolDefinition } from '../../../../_shared/index.js';
|
|
22
|
+
export declare function pythonArityCompatibility(def: SymbolDefinition, callsite: Callsite): 'compatible' | 'unknown' | 'incompatible';
|