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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
|
+
import { scoreImpactRisk, unusedAxesForImpactWalk } from '../../_shared/index.js';
|
|
11
12
|
import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
12
13
|
import { queryClassBeanMetadata } from './bean-metadata.js';
|
|
13
14
|
import { querySpringAopMetadata } from './aop-metadata.js';
|
|
@@ -23,7 +24,7 @@ import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/l
|
|
|
23
24
|
// at MCP server startup — crashes on unsupported Node ABI versions (#89)
|
|
24
25
|
// git utilities available if needed
|
|
25
26
|
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
26
|
-
import { parseDiffHunks, coalesceHunksByPath, hunksOverlapRange, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
|
|
27
|
+
import { parseDiffHunks, coalesceHunksByPath, hunksOverlapRange, findGitRootByDotGit, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
|
|
27
28
|
import { realpathSync } from 'fs';
|
|
28
29
|
import { listRegisteredRepos, cleanupOldKuzuFiles, canonicalizePath, getStoragePaths, loadMeta, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
|
|
29
30
|
import { GroupService, } from '../../core/group/service.js';
|
|
@@ -1178,23 +1179,56 @@ export class LocalBackend {
|
|
|
1178
1179
|
* - If only 1 repo, use it
|
|
1179
1180
|
* - If 0 or multiple without param, throw with helpful message
|
|
1180
1181
|
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1182
|
+
* Re-reads the registry before an omitted implicit target or after an
|
|
1183
|
+
* explicit miss, so long-running servers see newly indexed repositories.
|
|
1183
1184
|
*/
|
|
1184
1185
|
async resolveRepo(repoParam, branch) {
|
|
1185
|
-
|
|
1186
|
+
return this.selectToolRepository(repoParam, branch);
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Internal resolver variant for CLI/MCP tool routing and discovery.
|
|
1190
|
+
* - If repoParam is given, match by name or path
|
|
1191
|
+
* - If only 1 repo, use it
|
|
1192
|
+
* - If multiple repos exist and repoParam is omitted, callers may opt in to
|
|
1193
|
+
* the registered repo containing process.cwd()
|
|
1194
|
+
* - If 0 repos exist, or cwd cannot disambiguate multiple repos, throw
|
|
1195
|
+
*
|
|
1196
|
+
* Omitted-repo resolution re-reads the registry before accepting any
|
|
1197
|
+
* implicit target, including a cached singleton. A caller that just obtained
|
|
1198
|
+
* a fresh registry snapshot may disable that refresh explicitly.
|
|
1199
|
+
*/
|
|
1200
|
+
async selectToolRepository(repoParam, branch, options = {}) {
|
|
1201
|
+
const allowCwdDefault = options.allowCwdDefault === true;
|
|
1202
|
+
const mayRefresh = options.refreshRegistry !== false;
|
|
1203
|
+
let refreshed = false;
|
|
1204
|
+
// A cached singleton is also an implicit choice: another process may have
|
|
1205
|
+
// registered a second repo since init, which must not let a repo-less
|
|
1206
|
+
// mutating call bypass the multi-repo ambiguity guard.
|
|
1207
|
+
if (!repoParam && mayRefresh) {
|
|
1208
|
+
await this.refreshRepos();
|
|
1209
|
+
refreshed = true;
|
|
1210
|
+
}
|
|
1186
1211
|
let result;
|
|
1187
1212
|
try {
|
|
1188
|
-
result = this.resolveRepoFromCache(repoParam);
|
|
1213
|
+
result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
|
|
1189
1214
|
}
|
|
1190
1215
|
catch (err) {
|
|
1191
1216
|
if (!(err instanceof RegistryAmbiguousTargetError))
|
|
1192
1217
|
throw err;
|
|
1218
|
+
if (!mayRefresh || refreshed)
|
|
1219
|
+
throw err;
|
|
1193
1220
|
// Stale in-memory duplicate siblings can linger after unregister; refresh
|
|
1194
1221
|
// once before re-throwing so a resolved registry can disambiguate (#1658).
|
|
1195
1222
|
await this.refreshRepos();
|
|
1196
|
-
|
|
1197
|
-
result = this.resolveRepoFromCache(repoParam);
|
|
1223
|
+
refreshed = true;
|
|
1224
|
+
result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
|
|
1225
|
+
}
|
|
1226
|
+
// Explicit misses retain the existing one-refresh retry. Omitted targets
|
|
1227
|
+
// already refreshed above unless a same-snapshot caller opted out.
|
|
1228
|
+
if (!result && mayRefresh && !refreshed) {
|
|
1229
|
+
await this.refreshRepos();
|
|
1230
|
+
refreshed = true;
|
|
1231
|
+
result = this.resolveRepoFromCache(repoParam, allowCwdDefault);
|
|
1198
1232
|
}
|
|
1199
1233
|
if (result) {
|
|
1200
1234
|
// Issue: silent graph drift across sibling clones.
|
|
@@ -1208,15 +1242,6 @@ export class LocalBackend {
|
|
|
1208
1242
|
});
|
|
1209
1243
|
return this.applyBranchScope(result, branch);
|
|
1210
1244
|
}
|
|
1211
|
-
// Miss — refresh registry and try once more (skip if already refreshed above)
|
|
1212
|
-
if (!refreshedAfterAmbiguity) {
|
|
1213
|
-
await this.refreshRepos();
|
|
1214
|
-
}
|
|
1215
|
-
const retried = this.resolveRepoFromCache(repoParam);
|
|
1216
|
-
if (retried) {
|
|
1217
|
-
this.maybeWarnSiblingDrift(retried).catch(() => { });
|
|
1218
|
-
return this.applyBranchScope(retried, branch);
|
|
1219
|
-
}
|
|
1220
1245
|
// Still no match — throw with helpful message
|
|
1221
1246
|
if (this.repos.size === 0) {
|
|
1222
1247
|
throw new Error('No indexed repositories. Run: gitnexus analyze');
|
|
@@ -1349,7 +1374,7 @@ export class LocalBackend {
|
|
|
1349
1374
|
* Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
|
|
1350
1375
|
* multiple handles by name and cwd cannot disambiguate (#1658).
|
|
1351
1376
|
*/
|
|
1352
|
-
resolveRepoFromCache(repoParam) {
|
|
1377
|
+
resolveRepoFromCache(repoParam, allowCwdDefault = false) {
|
|
1353
1378
|
if (this.repos.size === 0)
|
|
1354
1379
|
return null;
|
|
1355
1380
|
if (repoParam) {
|
|
@@ -1378,6 +1403,9 @@ export class LocalBackend {
|
|
|
1378
1403
|
if (nameMatches.length === 1)
|
|
1379
1404
|
return nameMatches[0];
|
|
1380
1405
|
if (nameMatches.length > 1) {
|
|
1406
|
+
// Explicit duplicate aliases retain the legacy fail-closed contract:
|
|
1407
|
+
// only an exact cwd Git-root match may disambiguate them. Deepest path
|
|
1408
|
+
// containment is reserved for an omitted read-only repo (#3073).
|
|
1381
1409
|
const cwdPick = this.pickRepoHandleForCwd(nameMatches);
|
|
1382
1410
|
if (cwdPick)
|
|
1383
1411
|
return cwdPick;
|
|
@@ -1402,26 +1430,46 @@ export class LocalBackend {
|
|
|
1402
1430
|
if (this.repos.size === 1) {
|
|
1403
1431
|
return this.repos.values().next().value;
|
|
1404
1432
|
}
|
|
1433
|
+
if (allowCwdDefault) {
|
|
1434
|
+
const cwdPick = this.pickRepoHandleForCwd([...this.repos.values()], true);
|
|
1435
|
+
if (cwdPick)
|
|
1436
|
+
return cwdPick;
|
|
1437
|
+
}
|
|
1405
1438
|
return null; // Multiple repos, no param — ambiguous
|
|
1406
1439
|
}
|
|
1407
1440
|
/**
|
|
1408
|
-
*
|
|
1441
|
+
* Match process.cwd() against indexed repositories.
|
|
1409
1442
|
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1443
|
+
* Explicit duplicate aliases use exact Git-root matching only. Omitted
|
|
1444
|
+
* read-only calls opt into deepest containing-path selection. In that mode a
|
|
1445
|
+
* candidate must not sit above cwd's Git root, so an unindexed nested checkout
|
|
1446
|
+
* cannot fall through to an indexed ancestor. The `.git` ancestor fallback
|
|
1447
|
+
* preserves that boundary when the git executable is unavailable.
|
|
1414
1448
|
*/
|
|
1415
|
-
pickRepoHandleForCwd(candidates) {
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1449
|
+
pickRepoHandleForCwd(candidates, allowContaining = false) {
|
|
1450
|
+
const cwd = process.cwd();
|
|
1451
|
+
const normalize = (value) => {
|
|
1452
|
+
const canonical = canonicalizePath(value);
|
|
1453
|
+
return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
|
|
1454
|
+
};
|
|
1455
|
+
const isSameOrDescendant = (parent, child) => child === parent ||
|
|
1456
|
+
child.startsWith(parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`);
|
|
1457
|
+
const canonicalCwd = normalize(cwd);
|
|
1458
|
+
const cwdRoot = getGitRoot(cwd) ?? findGitRootByDotGit(cwd);
|
|
1459
|
+
const canonicalRoot = cwdRoot ? normalize(cwdRoot) : null;
|
|
1460
|
+
if (allowContaining) {
|
|
1461
|
+
const containing = candidates
|
|
1462
|
+
.map((handle) => ({ handle, repoPath: normalize(handle.repoPath) }))
|
|
1463
|
+
.filter(({ repoPath }) => isSameOrDescendant(repoPath, canonicalCwd))
|
|
1464
|
+
.filter(({ repoPath }) => !canonicalRoot || isSameOrDescendant(canonicalRoot, repoPath))
|
|
1465
|
+
.sort((a, b) => b.repoPath.length - a.repoPath.length);
|
|
1466
|
+
if (containing.length > 0)
|
|
1467
|
+
return containing[0].handle;
|
|
1468
|
+
}
|
|
1469
|
+
if (!canonicalRoot)
|
|
1418
1470
|
return null;
|
|
1419
|
-
const canonicalCwd = canonicalizePath(cwdRoot);
|
|
1420
1471
|
const cwdMatches = candidates.filter((handle) => {
|
|
1421
|
-
|
|
1422
|
-
return process.platform === 'win32'
|
|
1423
|
-
? stored.toLowerCase() === canonicalCwd.toLowerCase()
|
|
1424
|
-
: stored === canonicalCwd;
|
|
1472
|
+
return normalize(handle.repoPath) === canonicalRoot;
|
|
1425
1473
|
});
|
|
1426
1474
|
return cwdMatches.length === 1 ? cwdMatches[0] : null;
|
|
1427
1475
|
}
|
|
@@ -1818,7 +1866,7 @@ export class LocalBackend {
|
|
|
1818
1866
|
}
|
|
1819
1867
|
// Resolve repo from optional param (re-reads registry on miss). An optional
|
|
1820
1868
|
// `branch` param scopes the resolved handle to that branch's index (#2106).
|
|
1821
|
-
const repo = await this.
|
|
1869
|
+
const repo = await this.selectToolRepository(p.repo, p.branch, { allowCwdDefault: method !== 'rename' });
|
|
1822
1870
|
switch (method) {
|
|
1823
1871
|
case 'query':
|
|
1824
1872
|
return this.withToolStaleness(repo, await this.query(repo, p));
|
|
@@ -3000,7 +3048,22 @@ export class LocalBackend {
|
|
|
3000
3048
|
else if (isQualified) {
|
|
3001
3049
|
// Parenthesised because the kind filter below is appended with AND, which
|
|
3002
3050
|
// binds tighter than OR.
|
|
3003
|
-
|
|
3051
|
+
// #3074: a repo-relative file path (e.g. "supabase/functions/_shared/crypto.ts")
|
|
3052
|
+
// is the most natural way to name a File and is exactly what `target.filePath`
|
|
3053
|
+
// reports, but the old clause only matched `n.id` (= "File:<path>") or basename
|
|
3054
|
+
// `n.name`, so the same path the graph stores never resolved. Also match the
|
|
3055
|
+
// repo-relative `n.filePath` exactly and via an anchored suffix (segment-boundary
|
|
3056
|
+
// "ENDS WITH $suffix" where suffix is "/"+path) so "a.ts" does not spuriously
|
|
3057
|
+
// match "mylib/a.ts" — same anchoring used in detect_changes (#2915).
|
|
3058
|
+
const suffix = pathSuffixOf(name);
|
|
3059
|
+
// File-path terms must be scoped to File nodes — n.filePath is shared by
|
|
3060
|
+
// every symbol in the file, so an unlabeled predicate would turn
|
|
3061
|
+
// "src/actions.ts" into every symbol in that file (bot review #3084 P1).
|
|
3062
|
+
// LadybugDB does not allow label tests in WHERE (n:File), so scope via
|
|
3063
|
+
// id prefix — File nodes are `File:<path>`.
|
|
3064
|
+
whereClause = `WHERE (n.id = $symName OR n.name = $symName OR (n.id STARTS WITH $filePrefix AND (n.filePath = $symName OR n.filePath ENDS WITH $suffix)))`;
|
|
3065
|
+
queryParams.suffix = suffix;
|
|
3066
|
+
queryParams.filePrefix = 'File:';
|
|
3004
3067
|
}
|
|
3005
3068
|
else {
|
|
3006
3069
|
whereClause = `WHERE n.name = $symName`;
|
|
@@ -3079,7 +3142,7 @@ export class LocalBackend {
|
|
|
3079
3142
|
if (rows.length === 0)
|
|
3080
3143
|
return { kind: 'not_found' };
|
|
3081
3144
|
// Normalise row shape across object / tuple returns from LadybugDB.
|
|
3082
|
-
|
|
3145
|
+
let normalized = rows.map((r) => ({
|
|
3083
3146
|
id: (r.id ?? r[0]),
|
|
3084
3147
|
name: (r.name ?? r[1]),
|
|
3085
3148
|
type: (r.type ?? r[2] ?? ''),
|
|
@@ -3088,6 +3151,14 @@ export class LocalBackend {
|
|
|
3088
3151
|
endLine: (r.endLine ?? r[5]),
|
|
3089
3152
|
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
3090
3153
|
}));
|
|
3154
|
+
// An exact File path wins over anchored suffix candidates. Without this,
|
|
3155
|
+
// `lib/a.ts` and `src/lib/a.ts` both score as File candidates and turn an
|
|
3156
|
+
// otherwise unambiguous exact target into `ambiguous` (#3084 review P2).
|
|
3157
|
+
if (isQualified) {
|
|
3158
|
+
const exactFiles = normalized.filter((candidate) => candidate.id.startsWith('File:') && candidate.filePath === name);
|
|
3159
|
+
if (exactFiles.length > 0)
|
|
3160
|
+
normalized = exactFiles;
|
|
3161
|
+
}
|
|
3091
3162
|
// The COUNT can never legitimately be below the page it accompanies, so a
|
|
3092
3163
|
// value under `normalized.length` means the count leg failed or returned an
|
|
3093
3164
|
// unreadable shape. Keep the window size as the floor — reporting zero would
|
|
@@ -4942,6 +5013,7 @@ export class LocalBackend {
|
|
|
4942
5013
|
direction: params.direction,
|
|
4943
5014
|
suggestion,
|
|
4944
5015
|
recoverySuggestion,
|
|
5016
|
+
undetermined: true,
|
|
4945
5017
|
});
|
|
4946
5018
|
return pdgErr;
|
|
4947
5019
|
}
|
|
@@ -4949,7 +5021,7 @@ export class LocalBackend {
|
|
|
4949
5021
|
error: message,
|
|
4950
5022
|
target: { name: params.target },
|
|
4951
5023
|
direction: params.direction,
|
|
4952
|
-
impactedCount:
|
|
5024
|
+
impactedCount: null,
|
|
4953
5025
|
risk: 'UNKNOWN',
|
|
4954
5026
|
suggestion,
|
|
4955
5027
|
...(recoverySuggestion ? { recoverySuggestion } : {}),
|
|
@@ -5036,6 +5108,7 @@ export class LocalBackend {
|
|
|
5036
5108
|
`(single-repo PDG impact). Remove them or use mode:'callgraph' for cross-repo fan-out.`,
|
|
5037
5109
|
target: crossDepthTarget,
|
|
5038
5110
|
direction,
|
|
5111
|
+
undetermined: true,
|
|
5039
5112
|
});
|
|
5040
5113
|
return pdgErr;
|
|
5041
5114
|
}
|
|
@@ -5091,12 +5164,17 @@ export class LocalBackend {
|
|
|
5091
5164
|
error: `Target '${missing}' not found`,
|
|
5092
5165
|
target: notFoundTarget,
|
|
5093
5166
|
direction,
|
|
5167
|
+
undetermined: true,
|
|
5094
5168
|
})
|
|
5095
5169
|
: {
|
|
5096
5170
|
error: `Target '${missing}' not found`,
|
|
5097
5171
|
target: { name: target },
|
|
5098
5172
|
direction,
|
|
5099
|
-
|
|
5173
|
+
// #3074 follow-up: do not ship a normal-shaped 0/UNKNOWN blast radius
|
|
5174
|
+
// alongside the error — it reads as a real "nothing depends on this"
|
|
5175
|
+
// answer. Null marks UNDETERMINED (same as the ambiguous path) so a
|
|
5176
|
+
// consumer testing `impactedCount === 0` cannot misread a miss as safe.
|
|
5177
|
+
impactedCount: null,
|
|
5100
5178
|
risk: 'UNKNOWN',
|
|
5101
5179
|
};
|
|
5102
5180
|
}
|
|
@@ -5200,6 +5278,10 @@ export class LocalBackend {
|
|
|
5200
5278
|
score: Number(c.score.toFixed(2)),
|
|
5201
5279
|
impactedCount: summary?.impactedCount ?? 0,
|
|
5202
5280
|
risk: summary?.risk ?? 'UNKNOWN',
|
|
5281
|
+
...(summary?.riskSharedAxes !== undefined
|
|
5282
|
+
? { riskSharedAxes: summary.riskSharedAxes }
|
|
5283
|
+
: {}),
|
|
5284
|
+
...(summary?.riskScale !== undefined ? { riskScale: summary.riskScale } : {}),
|
|
5203
5285
|
direct: summary?.summary?.direct ?? 0,
|
|
5204
5286
|
...(summary?.riskNote !== undefined ? { riskNote: summary.riskNote } : {}),
|
|
5205
5287
|
// Carry the explanation with the verdict. The single-symbol path
|
|
@@ -6092,6 +6174,10 @@ export class LocalBackend {
|
|
|
6092
6174
|
const rawMaxChunks = process.env.IMPACT_MAX_CHUNKS?.trim();
|
|
6093
6175
|
const parsedMaxChunks = rawMaxChunks ? Number(rawMaxChunks) : Number.NaN;
|
|
6094
6176
|
const MAX_CHUNKS = Number.isInteger(parsedMaxChunks) && parsedMaxChunks >= 0 ? parsedMaxChunks : 10;
|
|
6177
|
+
let processQueryFailed = false;
|
|
6178
|
+
let moduleQueryFailed = false;
|
|
6179
|
+
let enrichmentDegraded = false;
|
|
6180
|
+
let moduleClassificationFailed = false;
|
|
6095
6181
|
// `skipEnrichment` (ambiguous #2129 per-candidate probes) bypasses the
|
|
6096
6182
|
// process/module aggregation passes entirely — those probes need only the
|
|
6097
6183
|
// count + a count-based risk, so paying the bounded-but-real enrichment cost
|
|
@@ -6125,7 +6211,12 @@ export class LocalBackend {
|
|
|
6125
6211
|
p.entryPointId AS entryPointId, hits, minStep, p.stepCount AS stepCount,
|
|
6126
6212
|
ep.name AS epName, labels(ep)[0] AS epType, ep.filePath AS epFilePath
|
|
6127
6213
|
ORDER BY pId
|
|
6128
|
-
`, { ids }).catch(() =>
|
|
6214
|
+
`, { ids }).catch((err) => {
|
|
6215
|
+
processQueryFailed = true;
|
|
6216
|
+
enrichmentDegraded = true;
|
|
6217
|
+
logQueryError('impact:process-chunk', err);
|
|
6218
|
+
return [];
|
|
6219
|
+
});
|
|
6129
6220
|
for (const row of rows) {
|
|
6130
6221
|
const pId = row.pId ?? row[0];
|
|
6131
6222
|
const epId = row.entryPointId ?? row[3] ?? row.pId ?? row[0];
|
|
@@ -6173,6 +6264,8 @@ export class LocalBackend {
|
|
|
6173
6264
|
}
|
|
6174
6265
|
}
|
|
6175
6266
|
catch (e) {
|
|
6267
|
+
processQueryFailed = true;
|
|
6268
|
+
enrichmentDegraded = true;
|
|
6176
6269
|
logQueryError('impact:process-chunk', e);
|
|
6177
6270
|
}
|
|
6178
6271
|
}
|
|
@@ -6187,7 +6280,11 @@ export class LocalBackend {
|
|
|
6187
6280
|
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
6188
6281
|
WHERE p.id IN $pIds AND s.id IN $ids
|
|
6189
6282
|
RETURN p.id AS pid, MIN(r.step) AS minStep
|
|
6190
|
-
`, { pIds, ids: allImpactedIds }).catch(() =>
|
|
6283
|
+
`, { pIds, ids: allImpactedIds }).catch((err) => {
|
|
6284
|
+
enrichmentDegraded = true;
|
|
6285
|
+
logQueryError('impact:process-chunk-backfill', err);
|
|
6286
|
+
return [];
|
|
6287
|
+
});
|
|
6191
6288
|
for (const mr of missingRows) {
|
|
6192
6289
|
const pid = mr.pid ?? mr[0];
|
|
6193
6290
|
const minStep = mr.minStep ?? mr[1];
|
|
@@ -6203,6 +6300,7 @@ export class LocalBackend {
|
|
|
6203
6300
|
}
|
|
6204
6301
|
}
|
|
6205
6302
|
catch (e) {
|
|
6303
|
+
enrichmentDegraded = true;
|
|
6206
6304
|
logQueryError('impact:process-chunk-backfill', e);
|
|
6207
6305
|
}
|
|
6208
6306
|
}
|
|
@@ -6250,7 +6348,12 @@ export class LocalBackend {
|
|
|
6250
6348
|
RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
|
|
6251
6349
|
ORDER BY hits DESC, name
|
|
6252
6350
|
LIMIT 20
|
|
6253
|
-
`, { ids: idsChunk }).catch(() =>
|
|
6351
|
+
`, { ids: idsChunk }).catch((err) => {
|
|
6352
|
+
moduleQueryFailed = true;
|
|
6353
|
+
enrichmentDegraded = true;
|
|
6354
|
+
logQueryError('impact:module-chunk', err);
|
|
6355
|
+
return [];
|
|
6356
|
+
});
|
|
6254
6357
|
for (const r of rows) {
|
|
6255
6358
|
const name = r.name ?? r[0] ?? null;
|
|
6256
6359
|
const hits = (r.hits ?? r[1]) || 0;
|
|
@@ -6260,6 +6363,8 @@ export class LocalBackend {
|
|
|
6260
6363
|
}
|
|
6261
6364
|
}
|
|
6262
6365
|
catch (e) {
|
|
6366
|
+
moduleQueryFailed = true;
|
|
6367
|
+
enrichmentDegraded = true;
|
|
6263
6368
|
logQueryError('impact:module-chunk', e);
|
|
6264
6369
|
}
|
|
6265
6370
|
};
|
|
@@ -6280,7 +6385,12 @@ export class LocalBackend {
|
|
|
6280
6385
|
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
6281
6386
|
WHERE s.id IN $ids
|
|
6282
6387
|
RETURN DISTINCT c.heuristicLabel AS name
|
|
6283
|
-
`, { ids: idsChunk }).catch(() =>
|
|
6388
|
+
`, { ids: idsChunk }).catch((err) => {
|
|
6389
|
+
enrichmentDegraded = true;
|
|
6390
|
+
moduleClassificationFailed = true;
|
|
6391
|
+
logQueryError('impact:direct-module-chunk', err);
|
|
6392
|
+
return [];
|
|
6393
|
+
});
|
|
6284
6394
|
for (const r of rows) {
|
|
6285
6395
|
const name = r.name ?? r[0] ?? null;
|
|
6286
6396
|
if (name)
|
|
@@ -6288,6 +6398,8 @@ export class LocalBackend {
|
|
|
6288
6398
|
}
|
|
6289
6399
|
}
|
|
6290
6400
|
catch (e) {
|
|
6401
|
+
enrichmentDegraded = true;
|
|
6402
|
+
moduleClassificationFailed = true;
|
|
6291
6403
|
logQueryError('impact:direct-module-chunk', e);
|
|
6292
6404
|
}
|
|
6293
6405
|
};
|
|
@@ -6308,47 +6420,35 @@ export class LocalBackend {
|
|
|
6308
6420
|
return {
|
|
6309
6421
|
name,
|
|
6310
6422
|
hits,
|
|
6311
|
-
impact:
|
|
6423
|
+
impact: moduleClassificationFailed
|
|
6424
|
+
? 'classification-unavailable'
|
|
6425
|
+
: directModuleNameSet.has(name)
|
|
6426
|
+
? 'direct'
|
|
6427
|
+
: 'indirect',
|
|
6312
6428
|
};
|
|
6313
6429
|
});
|
|
6314
6430
|
}
|
|
6315
6431
|
// Risk scoring
|
|
6316
6432
|
const processCount = affectedProcesses.length;
|
|
6317
6433
|
const moduleCount = affectedModules.length;
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
moduleCount >= 5 ||
|
|
6337
|
-
impacted.length >= 200) {
|
|
6338
|
-
risk = 'CRITICAL';
|
|
6339
|
-
}
|
|
6340
|
-
else if (directCount >= 15 ||
|
|
6341
|
-
processCount >= 3 ||
|
|
6342
|
-
moduleCount >= 3 ||
|
|
6343
|
-
impacted.length >= 100) {
|
|
6344
|
-
risk = 'HIGH';
|
|
6345
|
-
}
|
|
6346
|
-
else if (directCount >= 5 || impacted.length >= 30) {
|
|
6347
|
-
risk = 'MEDIUM';
|
|
6348
|
-
}
|
|
6349
|
-
else {
|
|
6350
|
-
risk = 'LOW';
|
|
6351
|
-
}
|
|
6434
|
+
const isFileTarget = symType === 'File' || String(symId).startsWith('File:');
|
|
6435
|
+
const unusedAxes = unusedAxesForImpactWalk({
|
|
6436
|
+
isFileTarget,
|
|
6437
|
+
skipEnrichment,
|
|
6438
|
+
maxChunks: MAX_CHUNKS,
|
|
6439
|
+
processQueryFailed,
|
|
6440
|
+
moduleQueryFailed,
|
|
6441
|
+
impactedCount: impacted.length,
|
|
6442
|
+
enrichmentTruncated: !skipEnrichment && MAX_CHUNKS > 0 && impacted.length > MAX_CHUNKS * CHUNK_SIZE,
|
|
6443
|
+
});
|
|
6444
|
+
const { risk, riskSharedAxes, riskScale } = scoreImpactRisk({
|
|
6445
|
+
direction,
|
|
6446
|
+
directCount,
|
|
6447
|
+
processCount,
|
|
6448
|
+
moduleCount,
|
|
6449
|
+
impactedCount: impacted.length,
|
|
6450
|
+
unusedAxes,
|
|
6451
|
+
});
|
|
6352
6452
|
// Build per-depth counts (always included, even in summaryOnly mode)
|
|
6353
6453
|
const byDepthCounts = {};
|
|
6354
6454
|
for (const [depth, items] of Object.entries(grouped)) {
|
|
@@ -6365,7 +6465,7 @@ export class LocalBackend {
|
|
|
6365
6465
|
target: {
|
|
6366
6466
|
id: symId,
|
|
6367
6467
|
name: sym.name || sym[1],
|
|
6368
|
-
type: symType,
|
|
6468
|
+
type: isFileTarget ? symType || 'File' : symType,
|
|
6369
6469
|
filePath: sym.filePath || sym[2],
|
|
6370
6470
|
...(beanMetadata ? { bean: beanMetadata } : {}),
|
|
6371
6471
|
...(aopMetadata ? { aop: aopMetadata } : {}),
|
|
@@ -6373,17 +6473,26 @@ export class LocalBackend {
|
|
|
6373
6473
|
direction,
|
|
6374
6474
|
impactedCount: impacted.length,
|
|
6375
6475
|
risk,
|
|
6476
|
+
riskSharedAxes,
|
|
6477
|
+
riskScale,
|
|
6376
6478
|
...(risk === 'UNKNOWN'
|
|
6377
6479
|
? {
|
|
6378
|
-
riskNote:
|
|
6379
|
-
'
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6480
|
+
riskNote: processQueryFailed || moduleQueryFailed
|
|
6481
|
+
? 'Risk is unresolved because process/module enrichment failed. Observed counts ' +
|
|
6482
|
+
'are lower bounds; retry impact before treating the change as safe.'
|
|
6483
|
+
: unusedAxes.some((axis) => axis.reason === 'enrichment-truncated')
|
|
6484
|
+
? 'Risk is unresolved because process/module enrichment was truncated. Observed ' +
|
|
6485
|
+
'counts are lower bounds; retry with a higher IMPACT_MAX_CHUNKS before ' +
|
|
6486
|
+
'treating the change as safe.'
|
|
6487
|
+
: 'No callers resolved. Absence of edges is not evidence the symbol is unused: ' +
|
|
6488
|
+
'a caller reaching it through a reference class this index does not record — ' +
|
|
6489
|
+
'plain-object property access, a bare-identifier read of a module-scope const — ' +
|
|
6490
|
+
'produces no edge to find. Confirm with a text search before treating the ' +
|
|
6491
|
+
'change as safe.',
|
|
6383
6492
|
}
|
|
6384
6493
|
: {}),
|
|
6385
6494
|
...epistemic,
|
|
6386
|
-
...(!traversalComplete && { partial: true }),
|
|
6495
|
+
...((!traversalComplete || enrichmentDegraded) && { partial: true }),
|
|
6387
6496
|
summary: {
|
|
6388
6497
|
direct: directCount,
|
|
6389
6498
|
processes_affected: processCount,
|
|
@@ -31,8 +31,10 @@ export declare function splitCalleeIds(raw: unknown): string[];
|
|
|
31
31
|
* Bump on any breaking change to the PDG result fields.
|
|
32
32
|
* v2: `startLine` in the result is now 1-based display (#2380), matching the
|
|
33
33
|
* context/query/impact tools (was 0-based).
|
|
34
|
+
* v3: error envelopes use `impactedCount: null` when a target cannot be resolved
|
|
35
|
+
* (#3074), so consumers cannot read a miss as a measured zero.
|
|
34
36
|
*/
|
|
35
|
-
export declare const PDG_RESULT_VERSION:
|
|
37
|
+
export declare const PDG_RESULT_VERSION: 3;
|
|
36
38
|
/** A reachable dependence block resolved to its source statement. */
|
|
37
39
|
export interface PdgStatement {
|
|
38
40
|
/** 1-based source line where the statement's block starts. */
|
|
@@ -229,7 +231,7 @@ export interface PdgInterproceduralImpact {
|
|
|
229
231
|
export interface PdgImpactBaseResult extends PdgImpactParityFields {
|
|
230
232
|
mode: 'pdg';
|
|
231
233
|
/** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
|
|
232
|
-
pdgResultVersion:
|
|
234
|
+
pdgResultVersion: 3;
|
|
233
235
|
target: PdgImpactTarget;
|
|
234
236
|
direction: 'upstream' | 'downstream';
|
|
235
237
|
impactedCount: number;
|
|
@@ -296,11 +298,11 @@ export interface PdgImpactDegradedResult extends PdgImpactBaseResult {
|
|
|
296
298
|
export interface PdgImpactErrorResult {
|
|
297
299
|
mode?: 'pdg';
|
|
298
300
|
/** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
|
|
299
|
-
pdgResultVersion:
|
|
301
|
+
pdgResultVersion: 3;
|
|
300
302
|
error: string;
|
|
301
303
|
target: PdgImpactTarget;
|
|
302
304
|
direction: 'upstream' | 'downstream';
|
|
303
|
-
impactedCount:
|
|
305
|
+
impactedCount: number | null;
|
|
304
306
|
risk: 'UNKNOWN';
|
|
305
307
|
suggestion?: string;
|
|
306
308
|
recoverySuggestion?: string;
|
|
@@ -313,6 +315,8 @@ export declare function makePdgImpactErrorResult(input: {
|
|
|
313
315
|
mode?: 'pdg';
|
|
314
316
|
suggestion?: string;
|
|
315
317
|
recoverySuggestion?: string;
|
|
318
|
+
/** True when analysis did not obtain a measured impact count. */
|
|
319
|
+
undetermined?: boolean;
|
|
316
320
|
}): PdgImpactErrorResult;
|
|
317
321
|
export declare function isPdgDegradedLayerStatus(layer: PdgLayerStatus): layer is PdgDegradedLayerStatus;
|
|
318
322
|
export declare function makePdgLayerDegradedResult(input: {
|
|
@@ -109,8 +109,10 @@ export function splitCalleeIds(raw) {
|
|
|
109
109
|
* Bump on any breaking change to the PDG result fields.
|
|
110
110
|
* v2: `startLine` in the result is now 1-based display (#2380), matching the
|
|
111
111
|
* context/query/impact tools (was 0-based).
|
|
112
|
+
* v3: error envelopes use `impactedCount: null` when a target cannot be resolved
|
|
113
|
+
* (#3074), so consumers cannot read a miss as a measured zero.
|
|
112
114
|
*/
|
|
113
|
-
export const PDG_RESULT_VERSION =
|
|
115
|
+
export const PDG_RESULT_VERSION = 3;
|
|
114
116
|
/**
|
|
115
117
|
* FU-B-2 intra-block def→use line walk. Given a block's self REACHING_DEF
|
|
116
118
|
* def→use line PAIRS (every `defLine → useLine` step decoded from the edge
|
|
@@ -469,7 +471,10 @@ export function makePdgImpactErrorResult(input) {
|
|
|
469
471
|
error: input.error,
|
|
470
472
|
target: input.target,
|
|
471
473
|
direction: input.direction,
|
|
472
|
-
|
|
474
|
+
// #3074 follow-up + P2 review: an unmeasured PDG result must not ship a
|
|
475
|
+
// confident-looking 0 blast radius. null marks UNDETERMINED, so a consumer
|
|
476
|
+
// testing === 0 cannot misread a miss or failed query as safe.
|
|
477
|
+
impactedCount: input.undetermined ? null : 0,
|
|
473
478
|
risk: 'UNKNOWN',
|
|
474
479
|
...(input.suggestion ? { suggestion: input.suggestion } : {}),
|
|
475
480
|
...(input.recoverySuggestion ? { recoverySuggestion: input.recoverySuggestion } : {}),
|
|
@@ -20,10 +20,14 @@ export declare class McpRepositoryPolicy {
|
|
|
20
20
|
private repoForArgs;
|
|
21
21
|
private normalizeToolArgs;
|
|
22
22
|
private listAllowedRepos;
|
|
23
|
-
|
|
23
|
+
toolSchemaRepoRequirements(backend: LocalBackend): Promise<{
|
|
24
|
+
readOnlyRequiresRepo: boolean;
|
|
25
|
+
mutatingRequiresRepo: boolean;
|
|
26
|
+
}>;
|
|
24
27
|
private listReposPage;
|
|
25
28
|
private callTool;
|
|
26
29
|
private resolveRepo;
|
|
30
|
+
private selectToolRepository;
|
|
27
31
|
assertResourceUri(uri: string): void;
|
|
28
32
|
resourceTemplateAllowed(uriTemplate: string): boolean;
|
|
29
33
|
toolAllowed(toolName: string): boolean;
|
|
@@ -134,10 +134,40 @@ export class McpRepositoryPolicy {
|
|
|
134
134
|
};
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
async
|
|
138
|
-
if (this.defaultRepo)
|
|
139
|
-
return false;
|
|
140
|
-
|
|
137
|
+
async toolSchemaRepoRequirements(backend) {
|
|
138
|
+
if (this.defaultRepo) {
|
|
139
|
+
return { readOnlyRequiresRepo: false, mutatingRequiresRepo: false };
|
|
140
|
+
}
|
|
141
|
+
// Runtime selection is based on the configured allowlist, not on which
|
|
142
|
+
// entries happen to remain visible in a later registry refresh. Keep the
|
|
143
|
+
// advertised schema aligned with repoForArgs() when that listing shrinks.
|
|
144
|
+
if (this.restricted) {
|
|
145
|
+
const requiresRepo = this.allowed.length > 1;
|
|
146
|
+
return {
|
|
147
|
+
readOnlyRequiresRepo: requiresRepo,
|
|
148
|
+
mutatingRequiresRepo: requiresRepo,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// One fresh listing supplies both schema decisions. Besides keeping the
|
|
152
|
+
// advertised contract internally consistent, this avoids doing two full
|
|
153
|
+
// per-repo staleness fan-outs for every tools/list request.
|
|
154
|
+
const visibleRepos = await this.listAllowedRepos(backend);
|
|
155
|
+
if (visibleRepos.length <= 1) {
|
|
156
|
+
return { readOnlyRequiresRepo: false, mutatingRequiresRepo: false };
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
// listAllowedRepos() refreshed this backend immediately above. Resolve
|
|
160
|
+
// against that exact cache snapshot instead of racing another registry
|
|
161
|
+
// read; only read-only schemas may advertise the cwd-derived default.
|
|
162
|
+
await backend.selectToolRepository(undefined, undefined, {
|
|
163
|
+
allowCwdDefault: true,
|
|
164
|
+
refreshRegistry: false,
|
|
165
|
+
});
|
|
166
|
+
return { readOnlyRequiresRepo: false, mutatingRequiresRepo: true };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return { readOnlyRequiresRepo: true, mutatingRequiresRepo: true };
|
|
170
|
+
}
|
|
141
171
|
}
|
|
142
172
|
async listReposPage(backend, params) {
|
|
143
173
|
const { limit, offset } = parseListReposPagination(params, {
|
|
@@ -186,6 +216,17 @@ export class McpRepositoryPolicy {
|
|
|
186
216
|
const selected = this.repoForArgs(repo === undefined ? undefined : { repo });
|
|
187
217
|
return backend.resolveRepo(selected?.path, branch);
|
|
188
218
|
}
|
|
219
|
+
async selectToolRepository(backend, repo, branch, options) {
|
|
220
|
+
if (!this.configured)
|
|
221
|
+
return backend.selectToolRepository(repo, branch, options);
|
|
222
|
+
if (!this.restricted) {
|
|
223
|
+
return backend.selectToolRepository(repo ?? this.defaultRepo?.path, branch, options);
|
|
224
|
+
}
|
|
225
|
+
const selected = this.repoForArgs(repo === undefined ? undefined : { repo });
|
|
226
|
+
// Restricted policies never allow cwd to select outside the configured
|
|
227
|
+
// set; once policy supplies an explicit path, the public resolver is enough.
|
|
228
|
+
return backend.resolveRepo(selected?.path, branch);
|
|
229
|
+
}
|
|
189
230
|
assertResourceUri(uri) {
|
|
190
231
|
if (!this.restricted)
|
|
191
232
|
return;
|
|
@@ -255,6 +296,9 @@ export class McpRepositoryPolicy {
|
|
|
255
296
|
if (property === 'resolveRepo') {
|
|
256
297
|
return (repo, branch) => policy.resolveRepo(target, repo, branch);
|
|
257
298
|
}
|
|
299
|
+
if (property === 'selectToolRepository') {
|
|
300
|
+
return (repo, branch, options) => policy.selectToolRepository(target, repo, branch, options);
|
|
301
|
+
}
|
|
258
302
|
if (property === 'getContext' && policy.restricted) {
|
|
259
303
|
return (repoId) => {
|
|
260
304
|
if (!repoId || !policy.uniqueAllowedContextNames.has(repoId.toLowerCase()))
|