gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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/README.md +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental derived-layer writeback helpers (#3016).
|
|
3
|
+
*
|
|
4
|
+
* The derived layers — Leiden communities, execution flows, and the FTS
|
|
5
|
+
* indexes — are graph-wide, so every analyze run rebuilt all three in full no
|
|
6
|
+
* matter how small the diff. A surgical incremental write can instead:
|
|
7
|
+
* - drop and rebuild only the FTS indexes whose tables hold rows in the
|
|
8
|
+
* write set (LadybugDB still cannot DML a table with a live FTS index —
|
|
9
|
+
* #2589 — so a table being written must still lose its index first);
|
|
10
|
+
* - leave the untouched tables' rows alone, so their indexes stay live;
|
|
11
|
+
* - reuse persisted Community/Process rows only when the file-hash diff is
|
|
12
|
+
* empty (no added, changed, or deleted files). Any content change can
|
|
13
|
+
* add, rename, or retarget symbols that Leiden and flow extraction
|
|
14
|
+
* consume — a no-deletion edit is not a validity proof.
|
|
15
|
+
*/
|
|
16
|
+
import { FTS_INDEXES } from '../search/fts-schema.js';
|
|
17
|
+
const FTS_TABLE_NAMES = new Set(FTS_INDEXES.map((i) => i.table));
|
|
18
|
+
/** The FTS-backed members of `tables`. */
|
|
19
|
+
export const ftsTablesAmong = (tables) => {
|
|
20
|
+
const out = new Set();
|
|
21
|
+
for (const table of tables) {
|
|
22
|
+
if (FTS_TABLE_NAMES.has(table))
|
|
23
|
+
out.add(table);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Whether a surgical incremental write may reuse the persisted derived layer.
|
|
29
|
+
*
|
|
30
|
+
* Deletions disqualify it: the persisted Community/Process rows and their
|
|
31
|
+
* MEMBER_OF / STEP_IN_PROCESS edges can reference nodes that no longer exist
|
|
32
|
+
* after this run, and nothing short of re-deriving can tell which.
|
|
33
|
+
*
|
|
34
|
+
* Added or content-changed files also disqualify it: they can introduce,
|
|
35
|
+
* rename, or retarget symbols and CALLS edges that Leiden and flow extraction
|
|
36
|
+
* consume. File-deletion-only was too weak a proof that the derived graph is
|
|
37
|
+
* still valid.
|
|
38
|
+
*/
|
|
39
|
+
export const shouldPreservePersistedDerivedGraph = (diff) => diff.deleted.length === 0 && diff.added.length === 0 && diff.changed.length === 0;
|
|
40
|
+
/**
|
|
41
|
+
* FTS-backed node tables that the fresh graph will WRITE rows into for
|
|
42
|
+
* `fileSet` — the inserting half of the DML.
|
|
43
|
+
*
|
|
44
|
+
* Callers must union this with a DB probe for the deleting half
|
|
45
|
+
* (`nodeTablesWithRowsForFiles`): a table whose last row in these files was
|
|
46
|
+
* just removed by the edit has nothing here, but still holds a stale row that
|
|
47
|
+
* the writeback must delete, and deleting it means taking its index down too.
|
|
48
|
+
*/
|
|
49
|
+
export const incrementalFtsTablesFromGraph = (graph, fileSet) => {
|
|
50
|
+
const touched = new Set();
|
|
51
|
+
graph.forEachNode((n) => {
|
|
52
|
+
const filePath = n.properties?.filePath;
|
|
53
|
+
if (!filePath || !fileSet.has(filePath))
|
|
54
|
+
return;
|
|
55
|
+
if (FTS_TABLE_NAMES.has(n.label))
|
|
56
|
+
touched.add(n.label);
|
|
57
|
+
});
|
|
58
|
+
return touched;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The node tables an incremental DETACH DELETE should target, given the FTS
|
|
62
|
+
* tables this run is rebuilding.
|
|
63
|
+
*
|
|
64
|
+
* Every non-FTS table (Folder, CodeElement, …) deletes as before. An FTS-backed
|
|
65
|
+
* table only deletes when its index is being rebuilt anyway, because deleting
|
|
66
|
+
* from it otherwise would mean DML against a live FTS index (#2589).
|
|
67
|
+
*/
|
|
68
|
+
export const nodeTablesForIncrementalDelete = (allNodeTables, rebuildingFtsTables) => allNodeTables.filter((tableName) => !FTS_TABLE_NAMES.has(tableName) || rebuildingFtsTables.has(tableName));
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* replaced, produce a smaller KnowledgeGraph that contains:
|
|
7
7
|
*
|
|
8
8
|
* - Every node whose `properties.filePath` is in `toWriteSet`.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - Graph-wide Community/Process nodes unless `includeDerivedGraphWide`
|
|
10
|
+
* is false (#3016 incremental preserve). Spring metadata placeholders
|
|
11
|
+
* are always included.
|
|
12
12
|
* - Every relationship where AT LEAST ONE endpoint is in the writable
|
|
13
13
|
* set above. Relationships entirely between unchanged-file nodes
|
|
14
14
|
* are skipped — their rows are still in the DB and re-inserting
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
* IMPORTS from the pre-pipeline DB) covers that case instead.
|
|
49
49
|
*/
|
|
50
50
|
import type { KnowledgeGraph } from '../graph/types.js';
|
|
51
|
-
export declare const extractChangedSubgraph: (fullGraph: KnowledgeGraph, toWriteSet: ReadonlySet<string
|
|
51
|
+
export declare const extractChangedSubgraph: (fullGraph: KnowledgeGraph, toWriteSet: ReadonlySet<string>, options?: {
|
|
52
|
+
includeDerivedGraphWide?: boolean;
|
|
53
|
+
}) => KnowledgeGraph;
|
|
52
54
|
/**
|
|
53
55
|
* Public — derive the EFFECTIVE write-set: `toWriteSet` expanded by one
|
|
54
56
|
* hop along every file-owned edge in the new graph that crosses the
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* replaced, produce a smaller KnowledgeGraph that contains:
|
|
7
7
|
*
|
|
8
8
|
* - Every node whose `properties.filePath` is in `toWriteSet`.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - Graph-wide Community/Process nodes unless `includeDerivedGraphWide`
|
|
10
|
+
* is false (#3016 incremental preserve). Spring metadata placeholders
|
|
11
|
+
* are always included.
|
|
12
12
|
* - Every relationship where AT LEAST ONE endpoint is in the writable
|
|
13
13
|
* set above. Relationships entirely between unchanged-file nodes
|
|
14
14
|
* are skipped — their rows are still in the DB and re-inserting
|
|
@@ -108,12 +108,14 @@ const indexNodeFilePaths = (fullGraph) => {
|
|
|
108
108
|
});
|
|
109
109
|
return idx;
|
|
110
110
|
};
|
|
111
|
-
export const extractChangedSubgraph = (fullGraph, toWriteSet) => {
|
|
111
|
+
export const extractChangedSubgraph = (fullGraph, toWriteSet, options) => {
|
|
112
112
|
const sub = createKnowledgeGraph();
|
|
113
113
|
const writableNodeIds = new Set();
|
|
114
|
+
const includeDerivedGraphWide = options?.includeDerivedGraphWide !== false;
|
|
114
115
|
fullGraph.forEachNode((n) => {
|
|
115
116
|
const filePath = n.properties?.filePath;
|
|
116
|
-
const
|
|
117
|
+
const derivedWide = includeDerivedGraphWide || (n.label !== 'Community' && n.label !== 'Process');
|
|
118
|
+
const include = (filePath && toWriteSet.has(filePath)) || (isGraphWideNode(n) && derivedWide);
|
|
117
119
|
if (include) {
|
|
118
120
|
sub.addNode(n);
|
|
119
121
|
writableNodeIds.add(n.id);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the index still reflect the files it actually covers?
|
|
3
|
+
*
|
|
4
|
+
* `status` used to answer this with a repo-wide `git status --porcelain`
|
|
5
|
+
* boolean, which says something different: whether the working tree differs
|
|
6
|
+
* from HEAD. Those two questions diverge in both directions. A scratch file,
|
|
7
|
+
* a build artifact, or a tracked file under a tool directory the indexer
|
|
8
|
+
* never reads makes the tree dirty while every indexed file is byte-current —
|
|
9
|
+
* and because `analyze` cannot commit or delete that file, the resulting
|
|
10
|
+
* "stale (re-run gitnexus analyze)" verdict was unclearable (#3077). It also
|
|
11
|
+
* misses the reverse case: reverting a file that was indexed while dirty
|
|
12
|
+
* leaves a clean tree over an index holding the pre-revert content.
|
|
13
|
+
*
|
|
14
|
+
* `meta.fileHashes` already records the exact set of files the last run
|
|
15
|
+
* covered, so the question can be answered directly. This module recomputes
|
|
16
|
+
* the coverage set with the same `walkRepositoryPaths` scan (ignore rules and
|
|
17
|
+
* dotfile handling stay shared) and the large-file cap recorded in
|
|
18
|
+
* `meta.indexCoverage`, hashes only the paths that can actually have changed
|
|
19
|
+
* since that run, and diffs against what was recorded.
|
|
20
|
+
*/
|
|
21
|
+
import type { RepoMeta } from '../storage/repo-meta.js';
|
|
22
|
+
/** Why the recorded coverage set could not be compared against disk at all. */
|
|
23
|
+
export type IndexContentUnmeasurableReason =
|
|
24
|
+
/** Metadata predates per-file hashes, or the run recorded none (non-git). */
|
|
25
|
+
'no-file-hashes'
|
|
26
|
+
/** The repository scan or hashing pass threw. */
|
|
27
|
+
| 'scan-failed';
|
|
28
|
+
/**
|
|
29
|
+
* A three-way verdict. `'unmeasurable'` is kept apart from `'current'` on
|
|
30
|
+
* purpose: it means the comparison never ran, which is not evidence the index
|
|
31
|
+
* is fresh. Legacy metadata without hashes still falls back to the working-tree
|
|
32
|
+
* check; a failed scan must not.
|
|
33
|
+
*/
|
|
34
|
+
export type IndexContentDrift = {
|
|
35
|
+
kind: 'current';
|
|
36
|
+
coveredFileCount: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'drifted';
|
|
39
|
+
changed: string[];
|
|
40
|
+
added: string[];
|
|
41
|
+
deleted: string[];
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'unmeasurable';
|
|
44
|
+
reason: IndexContentUnmeasurableReason;
|
|
45
|
+
};
|
|
46
|
+
export type IndexCoveragePolicy = NonNullable<RepoMeta['indexCoverage']>;
|
|
47
|
+
/**
|
|
48
|
+
* Compare the files recorded in `fileHashes` against the current working tree.
|
|
49
|
+
*
|
|
50
|
+
* `added` covers files the index would pick up but has never seen, so a new
|
|
51
|
+
* source file still reports stale — the index is genuinely incomplete then,
|
|
52
|
+
* and comparing only the recorded entries would wave that through.
|
|
53
|
+
*/
|
|
54
|
+
export declare const detectIndexContentDrift: (repoPath: string, fileHashes: Readonly<Record<string, string>> | undefined, coverage?: IndexCoveragePolicy) => Promise<IndexContentDrift>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does the index still reflect the files it actually covers?
|
|
3
|
+
*
|
|
4
|
+
* `status` used to answer this with a repo-wide `git status --porcelain`
|
|
5
|
+
* boolean, which says something different: whether the working tree differs
|
|
6
|
+
* from HEAD. Those two questions diverge in both directions. A scratch file,
|
|
7
|
+
* a build artifact, or a tracked file under a tool directory the indexer
|
|
8
|
+
* never reads makes the tree dirty while every indexed file is byte-current —
|
|
9
|
+
* and because `analyze` cannot commit or delete that file, the resulting
|
|
10
|
+
* "stale (re-run gitnexus analyze)" verdict was unclearable (#3077). It also
|
|
11
|
+
* misses the reverse case: reverting a file that was indexed while dirty
|
|
12
|
+
* leaves a clean tree over an index holding the pre-revert content.
|
|
13
|
+
*
|
|
14
|
+
* `meta.fileHashes` already records the exact set of files the last run
|
|
15
|
+
* covered, so the question can be answered directly. This module recomputes
|
|
16
|
+
* the coverage set with the same `walkRepositoryPaths` scan (ignore rules and
|
|
17
|
+
* dotfile handling stay shared) and the large-file cap recorded in
|
|
18
|
+
* `meta.indexCoverage`, hashes only the paths that can actually have changed
|
|
19
|
+
* since that run, and diffs against what was recorded.
|
|
20
|
+
*/
|
|
21
|
+
import { constants as fsConstants } from 'node:fs';
|
|
22
|
+
import { access } from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { walkRepositoryPaths } from './ingestion/filesystem-walker.js';
|
|
25
|
+
import { computeFileHashesDetailed } from '../storage/file-hash.js';
|
|
26
|
+
import { listWorkingTreeDirtyPaths } from '../storage/git.js';
|
|
27
|
+
import { isGitNexusManagedPath } from '../storage/gitnexus-managed-paths.js';
|
|
28
|
+
import { chunk } from '../lib/utils.js';
|
|
29
|
+
import { logger } from './logger.js';
|
|
30
|
+
const HASH_BATCH = 100;
|
|
31
|
+
const collectUnreadablePaths = async (repoPath, relPaths) => {
|
|
32
|
+
const unreadable = [];
|
|
33
|
+
for (const batch of chunk(relPaths, HASH_BATCH)) {
|
|
34
|
+
await Promise.all(batch.map(async (rel) => {
|
|
35
|
+
try {
|
|
36
|
+
await access(path.join(repoPath, rel), fsConstants.R_OK);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
unreadable.push(rel);
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
unreadable.sort();
|
|
44
|
+
return unreadable;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Compare the files recorded in `fileHashes` against the current working tree.
|
|
48
|
+
*
|
|
49
|
+
* `added` covers files the index would pick up but has never seen, so a new
|
|
50
|
+
* source file still reports stale — the index is genuinely incomplete then,
|
|
51
|
+
* and comparing only the recorded entries would wave that through.
|
|
52
|
+
*/
|
|
53
|
+
export const detectIndexContentDrift = async (repoPath, fileHashes, coverage) => {
|
|
54
|
+
if (!fileHashes || Object.keys(fileHashes).length === 0) {
|
|
55
|
+
return { kind: 'unmeasurable', reason: 'no-file-hashes' };
|
|
56
|
+
}
|
|
57
|
+
// Excluded from BOTH sides, or GitNexus's own output guarantees a mismatch:
|
|
58
|
+
// analyze rewrites AGENTS.md/CLAUDE.md after recording hashes, so they read
|
|
59
|
+
// as `added` on a first run and `changed` on every run after that — a fresh
|
|
60
|
+
// index would report itself stale forever.
|
|
61
|
+
const recorded = Object.fromEntries(Object.entries(fileHashes).filter(([rel]) => !isGitNexusManagedPath(rel)));
|
|
62
|
+
if (Object.keys(recorded).length === 0) {
|
|
63
|
+
return { kind: 'unmeasurable', reason: 'no-file-hashes' };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const scanned = await walkRepositoryPaths(repoPath, undefined, {
|
|
67
|
+
quiet: true,
|
|
68
|
+
maxFileSizeBytes: coverage?.maxFileSizeBytes,
|
|
69
|
+
});
|
|
70
|
+
const scannedPaths = scanned.map((file) => file.path).filter((p) => !isGitNexusManagedPath(p));
|
|
71
|
+
const scannedSet = new Set(scannedPaths);
|
|
72
|
+
const recordedSet = new Set(Object.keys(recorded));
|
|
73
|
+
// Legacy indexes have `fileHashes` but no `indexCoverage`. A later default
|
|
74
|
+
// cap would omit a still-present hashed file and call it deleted. Recorded
|
|
75
|
+
// paths that still exist stay in the coverage set even if this walk skipped
|
|
76
|
+
// them for size.
|
|
77
|
+
const recovered = new Set();
|
|
78
|
+
for (const rel of recordedSet) {
|
|
79
|
+
if (scannedSet.has(rel))
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
await access(path.join(repoPath, rel), fsConstants.R_OK);
|
|
83
|
+
recovered.add(rel);
|
|
84
|
+
scannedSet.add(rel);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Missing or unreadable: stays deleted / changed below.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const added = scannedPaths.filter((p) => !recordedSet.has(p)).sort();
|
|
91
|
+
const deleted = [...recordedSet].filter((p) => !scannedSet.has(p)).sort();
|
|
92
|
+
const intersection = [...recordedSet].filter((p) => scannedSet.has(p));
|
|
93
|
+
const dirtyNow = listWorkingTreeDirtyPaths(repoPath);
|
|
94
|
+
const dirtyAtIndex = coverage?.dirtyPaths;
|
|
95
|
+
const dirtyNowSet = dirtyNow === null ? null : new Set(dirtyNow);
|
|
96
|
+
const dirtyAtIndexSet = dirtyAtIndex === undefined ? undefined : new Set(dirtyAtIndex);
|
|
97
|
+
const hashCandidates = dirtyNowSet === null || dirtyAtIndexSet === undefined
|
|
98
|
+
? intersection
|
|
99
|
+
: intersection.filter((p) => dirtyAtIndexSet.has(p) || dirtyNowSet.has(p) || recovered.has(p));
|
|
100
|
+
const hashCandidateSet = new Set(hashCandidates);
|
|
101
|
+
const skipHash = intersection.filter((p) => !hashCandidateSet.has(p));
|
|
102
|
+
const unreadableFromAccess = await collectUnreadablePaths(repoPath, skipHash);
|
|
103
|
+
const unreadableSet = new Set(unreadableFromAccess);
|
|
104
|
+
const { hashes: hashed, unreadable: unreadableFromHash } = await computeFileHashesDetailed(repoPath, hashCandidates);
|
|
105
|
+
for (const p of unreadableFromHash)
|
|
106
|
+
unreadableSet.add(p);
|
|
107
|
+
const changed = [];
|
|
108
|
+
for (const p of intersection) {
|
|
109
|
+
if (unreadableSet.has(p)) {
|
|
110
|
+
changed.push(p);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const currentHash = hashed.get(p) ?? recorded[p];
|
|
114
|
+
if (currentHash !== recorded[p])
|
|
115
|
+
changed.push(p);
|
|
116
|
+
}
|
|
117
|
+
changed.sort();
|
|
118
|
+
if (changed.length === 0 && added.length === 0 && deleted.length === 0) {
|
|
119
|
+
return { kind: 'current', coveredFileCount: scannedSet.size };
|
|
120
|
+
}
|
|
121
|
+
return { kind: 'drifted', changed, added, deleted };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.warn({ err, repoPath }, 'index content drift scan failed');
|
|
125
|
+
return { kind: 'unmeasurable', reason: 'scan-failed' };
|
|
126
|
+
}
|
|
127
|
+
};
|
|
@@ -7,11 +7,21 @@ export interface ScannedFile {
|
|
|
7
7
|
export interface FilePath {
|
|
8
8
|
path: string;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export interface WalkRepositoryOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Suppress the operator-facing large-file notice. Set by read-only callers
|
|
13
|
+
* such as `status`, which reuse this scan purely to learn which files the
|
|
14
|
+
* index covers and must not emit analyze's progress commentary.
|
|
15
|
+
*/
|
|
16
|
+
quiet?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Override the large-file cap. `status` replays the bytes recorded at
|
|
19
|
+
* analyze time so `--max-file-size` / `GITNEXUS_MAX_FILE_SIZE` cannot
|
|
20
|
+
* silently drop a file that the index actually covers.
|
|
21
|
+
*/
|
|
22
|
+
maxFileSizeBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const walkRepositoryPaths: (repoPath: string, onProgress?: (current: number, total: number, filePath: string) => void, options?: WalkRepositoryOptions) => Promise<ScannedFile[]>;
|
|
15
25
|
/**
|
|
16
26
|
* Phase 2: Read file contents for a specific set of relative paths.
|
|
17
27
|
* Returns a Map for O(1) lookup. Silently skips files that fail to read.
|
|
@@ -39,9 +39,26 @@ const warnLargeFileSkip = (message) => {
|
|
|
39
39
|
* Phase 1: Scan repository — stat files to get paths + sizes, no content loaded.
|
|
40
40
|
* Memory: ~10MB for 100K files vs ~1GB+ with content.
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
const assertWalkRootIsDirectory = async (repoPath) => {
|
|
43
|
+
let st;
|
|
44
|
+
try {
|
|
45
|
+
st = await fs.stat(repoPath);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const code = err.code;
|
|
49
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
50
|
+
throw new Error(`walkRepositoryPaths: path does not exist: ${repoPath}`);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
if (!st.isDirectory()) {
|
|
55
|
+
throw new Error(`walkRepositoryPaths: not a directory: ${repoPath}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
export const walkRepositoryPaths = async (repoPath, onProgress, options = {}) => {
|
|
59
|
+
await assertWalkRootIsDirectory(repoPath);
|
|
43
60
|
const ignoreFilter = await createIgnoreFilter(repoPath);
|
|
44
|
-
const maxFileSizeBytes = getMaxFileSizeBytes();
|
|
61
|
+
const maxFileSizeBytes = options.maxFileSizeBytes ?? getMaxFileSizeBytes();
|
|
45
62
|
const filtered = await glob('**/*', {
|
|
46
63
|
cwd: repoPath,
|
|
47
64
|
nodir: true,
|
|
@@ -81,7 +98,7 @@ export const walkRepositoryPaths = async (repoPath, onProgress) => {
|
|
|
81
98
|
// scans. Canonicalize once at the scan boundary so every downstream phase sees
|
|
82
99
|
// the same repository order.
|
|
83
100
|
deduplicatedEntries.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
84
|
-
if (skippedLarge > 0) {
|
|
101
|
+
if (skippedLarge > 0 && !options.quiet) {
|
|
85
102
|
const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
86
103
|
const isOverrideUnset = !process.env.GITNEXUS_MAX_FILE_SIZE;
|
|
87
104
|
const suffix = isDefault ? ', likely generated/vendored' : '';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ParsedFile, Range, ScopeId } from '../../../../_shared/index.js';
|
|
2
|
+
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
3
|
+
import type { DiInjectionMatch } from '../../di-extractors/index.js';
|
|
4
|
+
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
|
5
|
+
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|
6
|
+
export interface SpringDynamicLookupFact {
|
|
7
|
+
readonly ownerScopeId: ScopeId;
|
|
8
|
+
readonly ownerRange: Range;
|
|
9
|
+
readonly receiverName: string;
|
|
10
|
+
readonly methodName: string;
|
|
11
|
+
readonly targetTypeName: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function springDynamicLookupCardinality(receiverName: string, methodName: string): DiInjectionMatch['cardinality'] | null;
|
|
14
|
+
export interface SpringDynamicLookupMetadataAdapter {
|
|
15
|
+
getFacts(filePath: string): readonly SpringDynamicLookupFact[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Attach AST-captured programmatic Spring lookups to the framework-neutral DI
|
|
19
|
+
* resolver. Java/Kotlin own syntax capture; this shared JVM/Spring seam owns
|
|
20
|
+
* import-aware type binding and metadata attachment.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSpringDynamicLookupMetadataAttacher(adapter: SpringDynamicLookupMetadataAdapter): (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes) => void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { SPRING_DI_INJECTION_SITES_PROPERTY } from '../../di-extractors/spring.js';
|
|
2
|
+
import { resolveCallerGraphId, resolveDefGraphId, } from '../../scope-resolution/graph-bridge/ids.js';
|
|
3
|
+
import { isClassLike, lookupBindingsAt } from '../../scope-resolution/scope/walkers.js';
|
|
4
|
+
const COLLECTION_LOOKUP_METHODS = new Set(['getBeans', 'getBeansOfType']);
|
|
5
|
+
const SINGLE_LOOKUP_METHODS = new Set(['getBean']);
|
|
6
|
+
/**
|
|
7
|
+
* Distinctive utility names plus conventional Spring context variable names.
|
|
8
|
+
* Generic locals remain recall-oriented because repositories often omit the
|
|
9
|
+
* third-party context type from the index; AST call/class-literal gates and
|
|
10
|
+
* import-aware target resolution prevent the raw-text false-positive class.
|
|
11
|
+
*/
|
|
12
|
+
const KNOWN_RECEIVERS = new Set([
|
|
13
|
+
'SpringContextUtil',
|
|
14
|
+
'SpringContextHolder',
|
|
15
|
+
'SpringBeanUtil',
|
|
16
|
+
'ApplicationContextProvider',
|
|
17
|
+
'BeanFactoryProvider',
|
|
18
|
+
'ApplicationContext',
|
|
19
|
+
'BeanFactory',
|
|
20
|
+
'ListableBeanFactory',
|
|
21
|
+
'applicationContext',
|
|
22
|
+
'context',
|
|
23
|
+
'ctx',
|
|
24
|
+
'appContext',
|
|
25
|
+
'beanFactory',
|
|
26
|
+
]);
|
|
27
|
+
export function springDynamicLookupCardinality(receiverName, methodName) {
|
|
28
|
+
const receiverSimpleName = receiverName.slice(receiverName.lastIndexOf('.') + 1);
|
|
29
|
+
if (!KNOWN_RECEIVERS.has(receiverSimpleName))
|
|
30
|
+
return null;
|
|
31
|
+
if (COLLECTION_LOOKUP_METHODS.has(methodName))
|
|
32
|
+
return 'collection';
|
|
33
|
+
if (SINGLE_LOOKUP_METHODS.has(methodName))
|
|
34
|
+
return 'single';
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function visibleTypeDefinitions(fact, indexes) {
|
|
38
|
+
const simpleName = fact.targetTypeName.slice(fact.targetTypeName.lastIndexOf('.') + 1);
|
|
39
|
+
let scopeId = fact.ownerScopeId;
|
|
40
|
+
while (scopeId !== null) {
|
|
41
|
+
const visible = lookupBindingsAt(scopeId, simpleName, indexes)
|
|
42
|
+
.map(({ def }) => def)
|
|
43
|
+
.filter((def) => isClassLike(def.type))
|
|
44
|
+
.filter((def) => !fact.targetTypeName.includes('.') || def.qualifiedName === fact.targetTypeName);
|
|
45
|
+
if (visible.length > 0) {
|
|
46
|
+
const unique = new Map(visible.map((def) => [def.nodeId, def]));
|
|
47
|
+
return [...unique.values()];
|
|
48
|
+
}
|
|
49
|
+
scopeId = indexes.scopeTree.getScope(scopeId)?.parent ?? null;
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
function resolveTargetTypeName(graph, fact, callerLanguage, nodeLookup, indexes) {
|
|
54
|
+
const graphIds = new Set();
|
|
55
|
+
for (const definition of visibleTypeDefinitions(fact, indexes)) {
|
|
56
|
+
const graphId = resolveDefGraphId(definition.filePath, definition, nodeLookup);
|
|
57
|
+
if (graphId === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const node = graph.getNode(graphId);
|
|
60
|
+
if ((node?.label === 'Class' ||
|
|
61
|
+
node?.label === 'Interface' ||
|
|
62
|
+
node?.label === 'Record' ||
|
|
63
|
+
node?.label === 'Enum') &&
|
|
64
|
+
node.properties.language === callerLanguage) {
|
|
65
|
+
graphIds.add(graphId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (graphIds.size !== 1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const targetId = graphIds.values().next().value;
|
|
71
|
+
if (targetId === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const target = graph.getNode(targetId);
|
|
74
|
+
if (target === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const qualifiedName = target.properties.qualifiedName;
|
|
77
|
+
return typeof qualifiedName === 'string' ? qualifiedName : target.properties.name;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Attach AST-captured programmatic Spring lookups to the framework-neutral DI
|
|
81
|
+
* resolver. Java/Kotlin own syntax capture; this shared JVM/Spring seam owns
|
|
82
|
+
* import-aware type binding and metadata attachment.
|
|
83
|
+
*/
|
|
84
|
+
export function createSpringDynamicLookupMetadataAttacher(adapter) {
|
|
85
|
+
return (graph, parsedFiles, nodeLookup, indexes) => {
|
|
86
|
+
for (const parsed of parsedFiles) {
|
|
87
|
+
for (const fact of adapter.getFacts(parsed.filePath)) {
|
|
88
|
+
const cardinality = springDynamicLookupCardinality(fact.receiverName, fact.methodName);
|
|
89
|
+
if (cardinality === null)
|
|
90
|
+
continue;
|
|
91
|
+
const callerId = resolveCallerGraphId(fact.ownerScopeId, indexes, nodeLookup, {
|
|
92
|
+
startLine: fact.ownerRange.startLine,
|
|
93
|
+
startCol: fact.ownerRange.startCol,
|
|
94
|
+
});
|
|
95
|
+
if (callerId === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const caller = graph.getNode(callerId);
|
|
98
|
+
if (caller === undefined ||
|
|
99
|
+
(caller.label !== 'Function' &&
|
|
100
|
+
caller.label !== 'Method' &&
|
|
101
|
+
caller.label !== 'Constructor')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const targetTypeName = resolveTargetTypeName(graph, fact, caller.properties.language, nodeLookup, indexes);
|
|
105
|
+
if (targetTypeName === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const match = {
|
|
108
|
+
targetTypeName,
|
|
109
|
+
cardinality,
|
|
110
|
+
edgeSource: 'site',
|
|
111
|
+
reason: `Spring dynamic lookup: ${fact.receiverName}.${fact.methodName}(${fact.targetTypeName})`,
|
|
112
|
+
};
|
|
113
|
+
// Singular lookups intentionally use the shared DI selection policy:
|
|
114
|
+
// a unique/@Primary candidate wins; unresolved multiplicity is an
|
|
115
|
+
// explicit 0.5-confidence fan-out rather than a guessed runtime winner.
|
|
116
|
+
const existing = caller.properties[SPRING_DI_INJECTION_SITES_PROPERTY];
|
|
117
|
+
caller.properties[SPRING_DI_INJECTION_SITES_PROPERTY] = [
|
|
118
|
+
...(Array.isArray(existing) ? existing : []),
|
|
119
|
+
match,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -297,6 +297,49 @@ interface LanguageProviderConfig {
|
|
|
297
297
|
* Default: undefined (no interface-inheritance route resolution).
|
|
298
298
|
*/
|
|
299
299
|
readonly extractRouteInheritanceTypes?: (tree: Parser.Tree, filePath: string) => SharedSpringType[];
|
|
300
|
+
/**
|
|
301
|
+
* Optional post-capture emission of synthetic structure members (nodes,
|
|
302
|
+
* symbols, ownership edges) that have no AST method node — e.g. Lombok
|
|
303
|
+
* accessors. Called once per file after the capture loop, at the same
|
|
304
|
+
* post-capture site as {@link extractDecoratorRoutes}.
|
|
305
|
+
*
|
|
306
|
+
* `classOwnersByNodeId` maps in-memory tree-sitter node ids of type
|
|
307
|
+
* declarations materialized in THIS file's capture loop to their graph
|
|
308
|
+
* node ids. Keys are never persisted; they exist only for the duration
|
|
309
|
+
* of the worker pass.
|
|
310
|
+
*
|
|
311
|
+
* Default: undefined (no synthetic structure members).
|
|
312
|
+
*/
|
|
313
|
+
readonly synthesizeStructureMembers?: (tree: Parser.Tree, filePath: string, classOwnersByNodeId: ReadonlyMap<number, string>) => {
|
|
314
|
+
nodes: ReadonlyArray<{
|
|
315
|
+
id: string;
|
|
316
|
+
label: string;
|
|
317
|
+
properties: Record<string, unknown>;
|
|
318
|
+
}>;
|
|
319
|
+
symbols: ReadonlyArray<{
|
|
320
|
+
filePath: string;
|
|
321
|
+
name: string;
|
|
322
|
+
nodeId: string;
|
|
323
|
+
type: string;
|
|
324
|
+
ownerId?: string;
|
|
325
|
+
parameterCount?: number;
|
|
326
|
+
requiredParameterCount?: number;
|
|
327
|
+
parameterTypes?: string[];
|
|
328
|
+
returnType?: string;
|
|
329
|
+
visibility?: string;
|
|
330
|
+
isStatic?: boolean;
|
|
331
|
+
isAbstract?: boolean;
|
|
332
|
+
isFinal?: boolean;
|
|
333
|
+
}>;
|
|
334
|
+
relationships: ReadonlyArray<{
|
|
335
|
+
id: string;
|
|
336
|
+
sourceId: string;
|
|
337
|
+
targetId: string;
|
|
338
|
+
type: string;
|
|
339
|
+
confidence: number;
|
|
340
|
+
reason: string;
|
|
341
|
+
}>;
|
|
342
|
+
};
|
|
300
343
|
/**
|
|
301
344
|
* Harvest this file's module-level string constants (#2391 core, #2980 Java
|
|
302
345
|
* parity) into the language-agnostic {@link ModuleConstants} shape, so the
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASP.NET Core ViewComponent convention support.
|
|
3
|
+
*
|
|
4
|
+
* Same bound as Spring Boot DI in Java/Kotlin: do not resolve into the SDK
|
|
5
|
+
* (`Microsoft.AspNetCore.Mvc.ViewComponent`, `IViewComponentHelper`,
|
|
6
|
+
* `Component.InvokeAsync` itself). Those types live outside the workspace.
|
|
7
|
+
* The only hop worth taking is the framework convention that lands on an
|
|
8
|
+
* **in-repo** class — `InvokeAsync("Foo")` → workspace `FooViewComponent`,
|
|
9
|
+
* just as a Spring `@Autowired IFoo` fans out to an in-repo `@Service`,
|
|
10
|
+
* not to `ApplicationContext`.
|
|
11
|
+
*
|
|
12
|
+
* Razor templates are not parsed as C# (markup + code would poison
|
|
13
|
+
* tree-sitter-c-sharp). A small Razor state machine extracts C# islands and
|
|
14
|
+
* markup tag helpers; C# files use a string/comment-aware lexer so attributes
|
|
15
|
+
* and literals are not mistaken for helper calls. Literal names are enough
|
|
16
|
+
* because the target catalog is already built from parsed `.cs` classes.
|
|
17
|
+
*/
|
|
18
|
+
import type { ParsedFile } from '../../../../_shared/index.js';
|
|
19
|
+
import type { KnowledgeGraph } from '../../../graph/types.js';
|
|
20
|
+
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
|
21
|
+
export interface RazorViewComponentConfig {
|
|
22
|
+
/** Repo-relative `.cshtml` path → extracted invocation names. */
|
|
23
|
+
readonly views: ReadonlyMap<string, readonly string[]>;
|
|
24
|
+
}
|
|
25
|
+
export interface ViewComponentAliasBind {
|
|
26
|
+
readonly className: string;
|
|
27
|
+
/** 1-based line of the type declaration (including leading attributes). */
|
|
28
|
+
readonly startLine: number;
|
|
29
|
+
/** 0-based column of the type declaration (including leading attributes). */
|
|
30
|
+
readonly startCol: number;
|
|
31
|
+
readonly aliases: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
/** In-repo C# `Component.InvokeAsync("X")` / `ViewComponent("X")` literals. */
|
|
34
|
+
export declare function extractCsharpViewComponentInvocations(source: string): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Explicit `[ViewComponent(Name = "...")]` aliases keyed to the following
|
|
37
|
+
* class declaration. Positional constructor arguments are ignored: the MVC
|
|
38
|
+
* attribute only exposes `Name` as a property.
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractViewComponentAliasBinds(source: string): ViewComponentAliasBind[];
|
|
41
|
+
/** Extract explicit `[ViewComponent(Name = "...")]` aliases by class name. */
|
|
42
|
+
export declare function extractViewComponentAliases(source: string): ReadonlyMap<string, readonly string[]>;
|
|
43
|
+
/** Extract statically resolvable ViewComponent names from one Razor template. */
|
|
44
|
+
export declare function extractRazorViewComponentInvocations(source: string): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Read Razor views once per C# resolution pass. The same ignore rules and file
|
|
47
|
+
* size ceiling as repository scanning are applied, and edge emission later
|
|
48
|
+
* additionally requires a live File node. This prevents ignored, oversized,
|
|
49
|
+
* or concurrently removed templates from entering the graph.
|
|
50
|
+
*/
|
|
51
|
+
export declare function loadRazorViewComponentConfig(repoRoot: string): Promise<RazorViewComponentConfig>;
|
|
52
|
+
/**
|
|
53
|
+
* Emit workspace File → in-repo ViewComponent Class CALLS edges.
|
|
54
|
+
*
|
|
55
|
+
* Targets are only Class nodes produced from this repo's `.cs` files. There is
|
|
56
|
+
* no lookup of ASP.NET SDK types; `: ViewComponent` in source is a naming
|
|
57
|
+
* hint, not a resolved EXTENDS edge to `Microsoft.AspNetCore.Mvc.ViewComponent`.
|
|
58
|
+
*
|
|
59
|
+
* Ambiguous component names fail closed: two in-repo classes claiming the
|
|
60
|
+
* same name is not evidence for picking either one.
|
|
61
|
+
*/
|
|
62
|
+
export declare function emitRazorViewComponentEdges(graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, config: RazorViewComponentConfig | undefined, csharpSources: ReadonlyMap<string, string>): void;
|