knodin 0.8.2 → 0.8.3
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 +29 -12
- package/dist/bin/cli.js +20 -3
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/index.js +911 -55
- package/dist/src/init-progress-worker.js +4 -40
- package/dist/src/progress-worker-runtime.js +46 -0
- package/dist/src/repair-progress-worker.js +3 -40
- package/dist/src/resource-reachability.js +456 -0
- package/dist/src/response-budget.js +69 -65
- package/dist/src/tools/knodin-tools.js +12 -7
- package/docs/BEHAVIORAL-CONTRACT.md +47 -5
- package/docs/CLI.md +10 -0
- package/docs/COMPARISON.md +10 -0
- package/docs/DEMO.md +49 -0
- package/docs/MCP.md +5 -0
- package/docs/releases/0.8.3.md +47 -0
- package/package.json +13 -1
- package/roadmap/competitive-roadmap.md +95 -0
package/dist/src/engine/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { compareBytes } from "../compare.js";
|
|
|
22
22
|
import { readIndexActivity } from "../index-activity.js";
|
|
23
23
|
import { runReadOnlyLspQuery } from "../lsp-readonly.js";
|
|
24
24
|
import { acquireRepairLease } from "../repair-lease.js";
|
|
25
|
+
import { analyzeResourceReachability, resourceFingerprint, } from "../resource-reachability.js";
|
|
25
26
|
import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snapshot.js";
|
|
26
27
|
import { KNODIN_VERSION } from "../version.js";
|
|
27
28
|
import * as ann from "./ann-hnsw.js";
|
|
@@ -65,6 +66,7 @@ export const REPO_WIDE_QUERY_PATTERNS = [
|
|
|
65
66
|
"surprising_connections",
|
|
66
67
|
"suggested_questions",
|
|
67
68
|
"architecture_overview",
|
|
69
|
+
"resource_reachability",
|
|
68
70
|
"batch_outline",
|
|
69
71
|
"project_overview",
|
|
70
72
|
"community",
|
|
@@ -661,6 +663,7 @@ const initPromises = new Map();
|
|
|
661
663
|
const watchers = new Map();
|
|
662
664
|
const WATCH_DEBOUNCE_MS = 50;
|
|
663
665
|
const watchQueues = new Map();
|
|
666
|
+
const repositoryEngineOwners = new Map();
|
|
664
667
|
/**
|
|
665
668
|
* Test-only reclamation for the single long-lived worker (isolate:false, C50).
|
|
666
669
|
* The module-level `dbInstances`/`watchers`/`watchQueues` caches are keyed by repo
|
|
@@ -718,7 +721,13 @@ export async function __evictRemovedRepoState() {
|
|
|
718
721
|
}
|
|
719
722
|
// Bumped whenever any repo is (re)indexed, so cached community-detection results
|
|
720
723
|
// below can be invalidated cheaply instead of recomputed on every call.
|
|
724
|
+
const KNODIN_SCHEMA_VERSION = 22;
|
|
725
|
+
const LAST_REBUILD_SCHEMA_VERSION = 22;
|
|
721
726
|
let indexGeneration = 0;
|
|
727
|
+
const resourceReachabilityCache = new Map();
|
|
728
|
+
const RESOURCE_REACHABILITY_CACHE_LIMIT = 32;
|
|
729
|
+
const resourceCorpusCache = new Map();
|
|
730
|
+
const RESOURCE_CORPUS_CACHE_LIMIT = 8;
|
|
722
731
|
const mapCache = new Map();
|
|
723
732
|
const minimalMapCache = new Map();
|
|
724
733
|
const MINIMAL_MAP_CACHE_LIMIT = 32;
|
|
@@ -3014,11 +3023,21 @@ function resolveImportPath(sourceFile, importSource, repoPath) {
|
|
|
3014
3023
|
// repoPath -> (package name -> { dir, entryFile }) for workspace/package-alias
|
|
3015
3024
|
// import resolution. Cached because it scans package.json files.
|
|
3016
3025
|
const packageMapCache = new Map();
|
|
3026
|
+
function invalidateResolutionCaches(repoPath) {
|
|
3027
|
+
const resolved = path.resolve(repoPath);
|
|
3028
|
+
packageMapCache.delete(resolved);
|
|
3029
|
+
const prefix = `${resolved}\0`;
|
|
3030
|
+
for (const key of reExportCache.keys())
|
|
3031
|
+
if (key.startsWith(prefix))
|
|
3032
|
+
reExportCache.delete(key);
|
|
3033
|
+
}
|
|
3017
3034
|
function getPackageMap(repoPath) {
|
|
3018
|
-
const
|
|
3019
|
-
|
|
3020
|
-
|
|
3035
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
3036
|
+
const cached = packageMapCache.get(resolvedRepoPath);
|
|
3037
|
+
if (cached?.generation === indexGeneration)
|
|
3038
|
+
return cached.value;
|
|
3021
3039
|
const map = new Map();
|
|
3040
|
+
let cacheable = true;
|
|
3022
3041
|
for (const rel of walkRepoFiles(repoPath, {
|
|
3023
3042
|
accept: (file) => path.posix.basename(file) === "package.json",
|
|
3024
3043
|
})) {
|
|
@@ -3032,9 +3051,14 @@ function getPackageMap(repoPath) {
|
|
|
3032
3051
|
});
|
|
3033
3052
|
}
|
|
3034
3053
|
}
|
|
3035
|
-
catch {
|
|
3054
|
+
catch {
|
|
3055
|
+
// A file can disappear or be rewritten while discovery is running. Do
|
|
3056
|
+
// not turn that transient failure into a process-lifetime empty mapping.
|
|
3057
|
+
cacheable = false;
|
|
3058
|
+
}
|
|
3036
3059
|
}
|
|
3037
|
-
|
|
3060
|
+
if (cacheable)
|
|
3061
|
+
packageMapCache.set(resolvedRepoPath, { generation: indexGeneration, value: map });
|
|
3038
3062
|
return map;
|
|
3039
3063
|
}
|
|
3040
3064
|
/** Best source entry file for a package: prefer source-y fields, else src/index, else index. */
|
|
@@ -3083,8 +3107,8 @@ const reExportCache = new Map();
|
|
|
3083
3107
|
function getReExports(repoPath, relFile) {
|
|
3084
3108
|
const cacheKey = `${repoPath}\0${relFile}`;
|
|
3085
3109
|
const cached = reExportCache.get(cacheKey);
|
|
3086
|
-
if (cached)
|
|
3087
|
-
return cached;
|
|
3110
|
+
if (cached?.generation === indexGeneration)
|
|
3111
|
+
return cached.value;
|
|
3088
3112
|
const named = new Map();
|
|
3089
3113
|
const stars = [];
|
|
3090
3114
|
try {
|
|
@@ -3107,9 +3131,12 @@ function getReExports(repoPath, relFile) {
|
|
|
3107
3131
|
stars.push(m[1]);
|
|
3108
3132
|
}
|
|
3109
3133
|
}
|
|
3110
|
-
catch {
|
|
3134
|
+
catch {
|
|
3135
|
+
// Never cache a transient read failure as an authoritative empty barrel.
|
|
3136
|
+
return { named, stars };
|
|
3137
|
+
}
|
|
3111
3138
|
const result = { named, stars };
|
|
3112
|
-
reExportCache.set(cacheKey, result);
|
|
3139
|
+
reExportCache.set(cacheKey, { generation: indexGeneration, value: result });
|
|
3113
3140
|
return result;
|
|
3114
3141
|
}
|
|
3115
3142
|
/**
|
|
@@ -3341,6 +3368,31 @@ function salesforceMetadataFile(relativePath) {
|
|
|
3341
3368
|
return { type: "approval", object: match[1], name: match[2] };
|
|
3342
3369
|
return null;
|
|
3343
3370
|
}
|
|
3371
|
+
/** Resolve only one locally declared, annotated Apex invocable method. */
|
|
3372
|
+
function findUniqueInvocableApexMethod(className, methodName, repoPath) {
|
|
3373
|
+
const file = findUniqueApexClassFilePath(className, repoPath);
|
|
3374
|
+
if (!file)
|
|
3375
|
+
return null;
|
|
3376
|
+
let source;
|
|
3377
|
+
try {
|
|
3378
|
+
source = fs.readFileSync(path.join(repoPath, file), "utf8");
|
|
3379
|
+
}
|
|
3380
|
+
catch {
|
|
3381
|
+
return null;
|
|
3382
|
+
}
|
|
3383
|
+
const matches = [];
|
|
3384
|
+
for (const match of source.matchAll(/@InvocableMethod\b[\s\S]{0,300}?\b(?:global|public)\s+static\s(?:\s|[^({;]*?(?<!\s))\s+([a-z]\w*)\s*\(/gi)) {
|
|
3385
|
+
if (!isExecutablePosition(source, match.index ?? 0) || match[1] !== methodName)
|
|
3386
|
+
continue;
|
|
3387
|
+
matches.push({
|
|
3388
|
+
file,
|
|
3389
|
+
method: methodName,
|
|
3390
|
+
line: source.slice(0, match.index ?? 0).split("\n").length,
|
|
3391
|
+
evidence: match[0].trim(),
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
return matches.length === 1 ? matches[0] : null;
|
|
3395
|
+
}
|
|
3344
3396
|
function staticXmlTagValues(content, tag) {
|
|
3345
3397
|
const uncommented = content.replace(/<!--[\s\S]*?-->/g, "");
|
|
3346
3398
|
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
@@ -3525,8 +3577,8 @@ function extractApexPlatformDependencies(content, repoPath) {
|
|
|
3525
3577
|
/** Index C20's static, deployable Salesforce DX metadata without interpreting formulas or runtime values. */
|
|
3526
3578
|
async function indexSalesforceMetadataFile(content, relativePath, repoPath, db, metadata) {
|
|
3527
3579
|
const dependencies = [];
|
|
3528
|
-
const add = (to, kind, sourceEvidence) => {
|
|
3529
|
-
dependencies.push({ to, kind, confidence: 1, sourceEvidence });
|
|
3580
|
+
const add = (to, kind, sourceEvidence, methodReference) => {
|
|
3581
|
+
dependencies.push({ to, kind, confidence: 1, sourceEvidence, methodReference });
|
|
3530
3582
|
};
|
|
3531
3583
|
const addField = (object, value, kind, evidence) => {
|
|
3532
3584
|
const qualified = value.includes(".") ? value : `${object}.${value}`;
|
|
@@ -3586,9 +3638,18 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
3586
3638
|
const target = /^([A-Za-z]\w*)\.([A-Za-z]\w*)$/.exec(entry.value);
|
|
3587
3639
|
if (!target)
|
|
3588
3640
|
continue;
|
|
3589
|
-
const
|
|
3590
|
-
if (
|
|
3591
|
-
|
|
3641
|
+
const invocable = findUniqueInvocableApexMethod(target[1], target[2], repoPath);
|
|
3642
|
+
if (!invocable)
|
|
3643
|
+
continue;
|
|
3644
|
+
const actionOffset = action.index ?? 0;
|
|
3645
|
+
const entryOffset = content.indexOf(entry.evidence, actionOffset);
|
|
3646
|
+
const line = content.slice(0, Math.max(actionOffset, entryOffset)).split("\n").length;
|
|
3647
|
+
add(invocable.file, "flow_apex_action", entry.evidence, {
|
|
3648
|
+
callee: invocable.method,
|
|
3649
|
+
calleeFile: invocable.file,
|
|
3650
|
+
line,
|
|
3651
|
+
column: 0,
|
|
3652
|
+
});
|
|
3592
3653
|
}
|
|
3593
3654
|
}
|
|
3594
3655
|
for (const entry of staticXmlTagValues(content, "extensionName")) {
|
|
@@ -3618,9 +3679,10 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
3618
3679
|
deleteSymbolsForFile(db, relativePath);
|
|
3619
3680
|
db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
|
|
3620
3681
|
db.run("DELETE FROM mcp_tools WHERE filePath = ?", [relativePath]);
|
|
3682
|
+
const flowSymbol = metadata.type === "flow" ? metadata.name : path.basename(relativePath, ".xml");
|
|
3621
3683
|
db.run(`INSERT INTO symbols (name, kind, filePath, startLine, endLine, startCol, endCol, summary)
|
|
3622
3684
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3623
|
-
|
|
3685
|
+
flowSymbol,
|
|
3624
3686
|
"salesforce-metadata",
|
|
3625
3687
|
relativePath,
|
|
3626
3688
|
1,
|
|
@@ -3629,6 +3691,14 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
3629
3691
|
0,
|
|
3630
3692
|
"Salesforce DX metadata (static declarations only; formulas and runtime targets are not resolved).",
|
|
3631
3693
|
]);
|
|
3694
|
+
const insertRef = db.prepare(`INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind, confidence)
|
|
3695
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
3696
|
+
for (const dependency of dependencies) {
|
|
3697
|
+
if (!dependency.methodReference)
|
|
3698
|
+
continue;
|
|
3699
|
+
insertRef.run(flowSymbol, relativePath, dependency.methodReference.callee, dependency.methodReference.calleeFile, dependency.methodReference.line, dependency.methodReference.column, dependency.kind, dependency.confidence);
|
|
3700
|
+
}
|
|
3701
|
+
insertRef.finalize();
|
|
3632
3702
|
const seen = new Set();
|
|
3633
3703
|
const insertDep = db.prepare("INSERT INTO dependencies (fromFile, toFile, kind, confidence, sourceEvidence) VALUES (?, ?, ?, ?, ?)");
|
|
3634
3704
|
for (const dependency of dependencies) {
|
|
@@ -3646,6 +3716,47 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
3646
3716
|
throw error;
|
|
3647
3717
|
}
|
|
3648
3718
|
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Revisit only indexed Flow files that literally name an incrementally changed
|
|
3721
|
+
* Apex class. This keeps persisted method edges coherent when an annotation,
|
|
3722
|
+
* overload, or declaration changes without requiring a repository-wide scan.
|
|
3723
|
+
*/
|
|
3724
|
+
async function refreshFlowsForApexFiles(db, repoPath, apexFiles) {
|
|
3725
|
+
const classNames = new Set(apexFiles
|
|
3726
|
+
.filter((file) => /^force-app\/main\/default\/classes\/[A-Za-z]\w*\.cls$/.test(file))
|
|
3727
|
+
.map((file) => path.basename(file, ".cls")));
|
|
3728
|
+
if (classNames.size === 0)
|
|
3729
|
+
return [];
|
|
3730
|
+
const flowFiles = db
|
|
3731
|
+
.query("SELECT DISTINCT filePath FROM symbols WHERE kind = 'salesforce-metadata' AND filePath LIKE 'force-app/main/default/flows/%.flow-meta.xml' ORDER BY filePath")
|
|
3732
|
+
.all();
|
|
3733
|
+
const refreshed = [];
|
|
3734
|
+
for (const { filePath } of flowFiles) {
|
|
3735
|
+
const absolute = path.join(repoPath, filePath);
|
|
3736
|
+
let content;
|
|
3737
|
+
try {
|
|
3738
|
+
content = fs.readFileSync(absolute, "utf8");
|
|
3739
|
+
}
|
|
3740
|
+
catch {
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3743
|
+
const mentionsChangedClass = [...content.matchAll(/<actionCalls>([\s\S]*?)<\/actionCalls>/g)]
|
|
3744
|
+
.filter((match) => /<actionType>apex<\/actionType>/.test(match[1]))
|
|
3745
|
+
.some((match) => staticXmlTagValues(match[1], "actionName").some((entry) => {
|
|
3746
|
+
const target = /^([A-Za-z]\w*)\.([A-Za-z]\w*)$/.exec(entry.value);
|
|
3747
|
+
return target ? classNames.has(target[1]) : false;
|
|
3748
|
+
}));
|
|
3749
|
+
if (!mentionsChangedClass)
|
|
3750
|
+
continue;
|
|
3751
|
+
const metadata = salesforceMetadataFile(filePath);
|
|
3752
|
+
if (!metadata || metadata.type !== "flow")
|
|
3753
|
+
continue;
|
|
3754
|
+
await indexSalesforceMetadataFile(content, filePath, repoPath, db, metadata);
|
|
3755
|
+
recordIndexState(db, repoPath, filePath);
|
|
3756
|
+
refreshed.push(filePath);
|
|
3757
|
+
}
|
|
3758
|
+
return refreshed;
|
|
3759
|
+
}
|
|
3649
3760
|
/** Index the source-only, statically resolvable portion of one Salesforce LWC bundle member. */
|
|
3650
3761
|
async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
|
|
3651
3762
|
const dependencies = [
|
|
@@ -5873,6 +5984,392 @@ async function indexFile(absolutePath, relativePath, repoPath, db, unparsed) {
|
|
|
5873
5984
|
tallyOne(unparsed, extensionBucket(relativePath));
|
|
5874
5985
|
}
|
|
5875
5986
|
}
|
|
5987
|
+
/**
|
|
5988
|
+
* Rebuild the deliberately bounded TypeScript DI overlay from current source.
|
|
5989
|
+
* The overlay is derived into the existing references store, so every existing
|
|
5990
|
+
* graph surface sees the exact same edges and incremental indexing can replace
|
|
5991
|
+
* stale wiring atomically without a second schema or cache.
|
|
5992
|
+
*/
|
|
5993
|
+
function reconcileTypeScriptDi(db, repoPath) {
|
|
5994
|
+
const files = collectRepoFiles(repoPath).filter((file) => (file.endsWith(".ts") || file.endsWith(".tsx")) && !file.endsWith(".d.ts"));
|
|
5995
|
+
const frameworkFiles = files.filter((file) => {
|
|
5996
|
+
try {
|
|
5997
|
+
const source = fs.readFileSync(path.join(repoPath, file), "utf8");
|
|
5998
|
+
const importsOnly = maskBlockComments(source, (text) => text.replace(/[^\n]/g, " ")).replace(/(^|\s)\/\/.*$/gm, (text, prefix) => prefix + " ".repeat(text.length - prefix.length));
|
|
5999
|
+
return /^\s*import\b[^\n]*\bfrom\s+["'](?:inversify|tsyringe)["']/m.test(importsOnly);
|
|
6000
|
+
}
|
|
6001
|
+
catch {
|
|
6002
|
+
return false;
|
|
6003
|
+
}
|
|
6004
|
+
});
|
|
6005
|
+
const frameworkFileSet = new Set(frameworkFiles);
|
|
6006
|
+
for (const row of db
|
|
6007
|
+
.query("SELECT id, filePath FROM symbols WHERE kind = 'di_module'")
|
|
6008
|
+
.all())
|
|
6009
|
+
if (!frameworkFileSet.has(row.filePath))
|
|
6010
|
+
db.run("DELETE FROM symbols WHERE id = ?", [row.id]);
|
|
6011
|
+
for (const file of frameworkFiles) {
|
|
6012
|
+
const present = db
|
|
6013
|
+
.query("SELECT 1 AS present FROM symbols WHERE kind = 'di_module' AND filePath = ? LIMIT 1")
|
|
6014
|
+
.get(file);
|
|
6015
|
+
if (!present)
|
|
6016
|
+
db.run("INSERT INTO symbols(name, kind, filePath, startLine, endLine, startCol, endCol, summary) VALUES (?, 'di_module', ?, 1, 1, 0, 0, ?)", [`di-module:${file}`, file, `Static DI module for ${file}`]);
|
|
6017
|
+
}
|
|
6018
|
+
if (frameworkFiles.length > 0)
|
|
6019
|
+
persistSymbolIdentities(db, repoPath, frameworkFiles);
|
|
6020
|
+
const symbols = db.query("SELECT * FROM symbols").all();
|
|
6021
|
+
const byFile = new Map();
|
|
6022
|
+
for (const symbol of symbols) {
|
|
6023
|
+
const rows = byFile.get(symbol.filePath) ?? [];
|
|
6024
|
+
rows.push(symbol);
|
|
6025
|
+
byFile.set(symbol.filePath, rows);
|
|
6026
|
+
}
|
|
6027
|
+
const candidates = [];
|
|
6028
|
+
const bindingCandidates = [];
|
|
6029
|
+
const resolutionCandidates = [];
|
|
6030
|
+
const ownerAt = (file, line) => {
|
|
6031
|
+
const rows = byFile.get(file) ?? [];
|
|
6032
|
+
return (rows
|
|
6033
|
+
.filter((row) => row.startLine <= line &&
|
|
6034
|
+
row.endLine >= line &&
|
|
6035
|
+
["function", "method", "class"].includes(row.kind))
|
|
6036
|
+
.sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine) || (a.kind === "class" ? 1 : -1))[0] ?? rows.find((row) => row.kind === "di_module"));
|
|
6037
|
+
};
|
|
6038
|
+
const exactSymbol = (file, name) => {
|
|
6039
|
+
const matches = (byFile.get(file) ?? []).filter((row) => row.name === name);
|
|
6040
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
6041
|
+
};
|
|
6042
|
+
for (const file of files) {
|
|
6043
|
+
let source;
|
|
6044
|
+
try {
|
|
6045
|
+
source = fs.readFileSync(path.join(repoPath, file), "utf8");
|
|
6046
|
+
}
|
|
6047
|
+
catch {
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
const masked = maskBlockComments(source, (text) => text.replace(/[^\n]/g, " ")).replace(/(^|\s)\/\/.*$/gm, (text, prefix) => prefix + " ".repeat(text.length - prefix.length));
|
|
6051
|
+
const codePosition = new Uint8Array(masked.length).fill(1);
|
|
6052
|
+
let quote = "";
|
|
6053
|
+
for (let index = 0; index < masked.length; index++) {
|
|
6054
|
+
const char = masked[index];
|
|
6055
|
+
if (quote) {
|
|
6056
|
+
codePosition[index] = 0;
|
|
6057
|
+
if (char === "\\") {
|
|
6058
|
+
if (index + 1 < masked.length)
|
|
6059
|
+
codePosition[++index] = 0;
|
|
6060
|
+
}
|
|
6061
|
+
else if (char === quote)
|
|
6062
|
+
quote = "";
|
|
6063
|
+
}
|
|
6064
|
+
else if (char === '"' || char === "'" || char === "`") {
|
|
6065
|
+
quote = char;
|
|
6066
|
+
codePosition[index] = 0;
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
const isCodeMatch = (match) => codePosition[match.index ?? 0] === 1;
|
|
6070
|
+
const hasInversify = [...masked.matchAll(/\bfrom\s+["']inversify["']/g)].some(isCodeMatch);
|
|
6071
|
+
const hasTsyringe = [...masked.matchAll(/\bfrom\s+["']tsyringe["']/g)].some(isCodeMatch);
|
|
6072
|
+
if (!hasInversify && !hasTsyringe)
|
|
6073
|
+
continue;
|
|
6074
|
+
const imports = new Map();
|
|
6075
|
+
for (const match of masked.matchAll(/\bimport\s+(?:type\s+)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g)) {
|
|
6076
|
+
if (!isCodeMatch(match))
|
|
6077
|
+
continue;
|
|
6078
|
+
for (const specifier of match[1].split(",")) {
|
|
6079
|
+
const parts = specifier
|
|
6080
|
+
.trim()
|
|
6081
|
+
.replace(/^type\s+/, "")
|
|
6082
|
+
.split(/\s+as\s+/);
|
|
6083
|
+
if (parts[0])
|
|
6084
|
+
imports.set(parts[1] ?? parts[0], { original: parts[0], module: match[2] });
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
for (const match of masked.matchAll(/\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*["']([^"']+)["']/g))
|
|
6088
|
+
if (isCodeMatch(match))
|
|
6089
|
+
imports.set(match[1], { original: match[1], module: match[2] });
|
|
6090
|
+
const receivers = new Set();
|
|
6091
|
+
for (const [local, binding] of imports)
|
|
6092
|
+
if (binding.module === "tsyringe" && binding.original === "container")
|
|
6093
|
+
receivers.add(local);
|
|
6094
|
+
if (hasInversify) {
|
|
6095
|
+
const containerTypes = [...imports]
|
|
6096
|
+
.filter(([, binding]) => binding.module === "inversify" && binding.original === "Container")
|
|
6097
|
+
.map(([local]) => local);
|
|
6098
|
+
for (const typeName of containerTypes) {
|
|
6099
|
+
const escapedType = typeName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6100
|
+
for (const match of masked.matchAll(new RegExp(String.raw `\b(?:const|let|var)\s+(\w+)\s*=\s*new\s+${escapedType}\b`, "g")))
|
|
6101
|
+
if (isCodeMatch(match))
|
|
6102
|
+
receivers.add(match[1]);
|
|
6103
|
+
for (const match of masked.matchAll(new RegExp(String.raw `\b([A-Za-z_$][\w$]*)\s*:\s*${escapedType}\b`, "g")))
|
|
6104
|
+
if (isCodeMatch(match))
|
|
6105
|
+
receivers.add(match[1]);
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
const point = (index) => getLineAndColumnForIndex(source, index);
|
|
6109
|
+
const conditionalAt = (index) => {
|
|
6110
|
+
const stack = [];
|
|
6111
|
+
for (let cursor = 0; cursor < index; cursor++) {
|
|
6112
|
+
if (masked[cursor] === "{") {
|
|
6113
|
+
const prefix = masked.slice(Math.max(0, cursor - 240), cursor);
|
|
6114
|
+
stack.push(/\b(?:if|switch|for|while)\s*\([^{}]*\)\s*$/.test(prefix));
|
|
6115
|
+
}
|
|
6116
|
+
else if (masked[cursor] === "}")
|
|
6117
|
+
stack.pop();
|
|
6118
|
+
}
|
|
6119
|
+
if (stack.includes(true))
|
|
6120
|
+
return true;
|
|
6121
|
+
const boundary = Math.max(masked.lastIndexOf(";", index - 1), masked.lastIndexOf("{", index - 1), masked.lastIndexOf("}", index - 1));
|
|
6122
|
+
const prefix = masked.slice(boundary + 1, index);
|
|
6123
|
+
return /\b(?:if|switch|for|while)\s*\(|&&|\|\||\?|=>/.test(prefix);
|
|
6124
|
+
};
|
|
6125
|
+
const canonicalToken = (expression) => {
|
|
6126
|
+
const token = expression.trim();
|
|
6127
|
+
const literal = /^(?:"([^"\n]*)"|'([^'\n]*)')$/.exec(token);
|
|
6128
|
+
if (literal)
|
|
6129
|
+
return { value: `literal:${literal[1] ?? literal[2]}` };
|
|
6130
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(token))
|
|
6131
|
+
return { reason: "Token is dynamic or computed." };
|
|
6132
|
+
const imported = imports.get(token);
|
|
6133
|
+
if (imported) {
|
|
6134
|
+
const moduleFile = resolveModuleToFile(file, imported.module, repoPath);
|
|
6135
|
+
if (!moduleFile)
|
|
6136
|
+
return { reason: `Token import ${token} does not resolve uniquely.` };
|
|
6137
|
+
const definitionFile = followReExports(repoPath, moduleFile, imported.original, 0);
|
|
6138
|
+
if (!exactSymbol(definitionFile, imported.original))
|
|
6139
|
+
return { reason: `Token ${token} does not have one indexed declaration.` };
|
|
6140
|
+
return { value: `symbol:${definitionFile}#${imported.original}` };
|
|
6141
|
+
}
|
|
6142
|
+
if (!exactSymbol(file, token))
|
|
6143
|
+
return { reason: `Token ${token} does not have one indexed declaration.` };
|
|
6144
|
+
return { value: `symbol:${file}#${token}` };
|
|
6145
|
+
};
|
|
6146
|
+
const implementation = (name) => {
|
|
6147
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name))
|
|
6148
|
+
return undefined;
|
|
6149
|
+
const imported = imports.get(name);
|
|
6150
|
+
if (imported) {
|
|
6151
|
+
const moduleFile = resolveModuleToFile(file, imported.module, repoPath);
|
|
6152
|
+
if (!moduleFile)
|
|
6153
|
+
return undefined;
|
|
6154
|
+
return exactSymbol(followReExports(repoPath, moduleFile, imported.original, 0), imported.original);
|
|
6155
|
+
}
|
|
6156
|
+
return exactSymbol(file, name);
|
|
6157
|
+
};
|
|
6158
|
+
const addOmission = (index, kind, reason) => {
|
|
6159
|
+
if (codePosition[index] !== 1)
|
|
6160
|
+
return;
|
|
6161
|
+
const where = point(index);
|
|
6162
|
+
candidates.push({
|
|
6163
|
+
type: "omission",
|
|
6164
|
+
owner: ownerAt(file, where.line),
|
|
6165
|
+
file,
|
|
6166
|
+
line: where.line,
|
|
6167
|
+
column: where.column,
|
|
6168
|
+
kind,
|
|
6169
|
+
reason,
|
|
6170
|
+
evidence: source.split(/\r?\n/)[where.line - 1]?.trim().slice(0, 240) ?? "",
|
|
6171
|
+
});
|
|
6172
|
+
};
|
|
6173
|
+
const registryFor = (framework, receiver, index) => {
|
|
6174
|
+
if (framework === "tsyringe")
|
|
6175
|
+
return "tsyringe:global";
|
|
6176
|
+
const before = masked.slice(0, index);
|
|
6177
|
+
if (new RegExp(String.raw `\b(?:const|let|var)\s+${receiver}\s*=\s*new\s+`).test(before))
|
|
6178
|
+
return `inversify:${file}:module:${receiver}`;
|
|
6179
|
+
const where = point(index);
|
|
6180
|
+
const owner = ownerAt(file, where.line);
|
|
6181
|
+
return `inversify:${file}:${owner?.identity ?? owner?.name ?? "module"}:${receiver}`;
|
|
6182
|
+
};
|
|
6183
|
+
const addBinding = (index, tokenExpression, implementationName, framework, receiver) => {
|
|
6184
|
+
if (codePosition[index] !== 1)
|
|
6185
|
+
return;
|
|
6186
|
+
if (conditionalAt(index))
|
|
6187
|
+
return addOmission(index, "di_conditional_registration", "Conditional registration is runtime-dependent.");
|
|
6188
|
+
const where = point(index);
|
|
6189
|
+
const token = canonicalToken(tokenExpression);
|
|
6190
|
+
if (!token.value)
|
|
6191
|
+
return addOmission(index, "di_dynamic_token", token.reason ?? "Dynamic token.");
|
|
6192
|
+
const target = implementation(implementationName.trim());
|
|
6193
|
+
if (!target)
|
|
6194
|
+
return addOmission(index, "di_unresolved_implementation", `Implementation ${implementationName.trim()} does not resolve to one indexed symbol.`);
|
|
6195
|
+
bindingCandidates.push({
|
|
6196
|
+
type: "binding",
|
|
6197
|
+
token: token.value,
|
|
6198
|
+
framework,
|
|
6199
|
+
registry: registryFor(framework, receiver, index),
|
|
6200
|
+
implementation: target,
|
|
6201
|
+
owner: ownerAt(file, where.line),
|
|
6202
|
+
file,
|
|
6203
|
+
line: where.line,
|
|
6204
|
+
column: where.column,
|
|
6205
|
+
evidence: source.split(/\r?\n/)[where.line - 1]?.trim().slice(0, 240) ?? "",
|
|
6206
|
+
});
|
|
6207
|
+
};
|
|
6208
|
+
const addResolution = (index, tokenExpression, framework, receiver) => {
|
|
6209
|
+
if (codePosition[index] !== 1)
|
|
6210
|
+
return;
|
|
6211
|
+
if (conditionalAt(index))
|
|
6212
|
+
return addOmission(index, "di_conditional_resolution", "Conditional resolution is runtime-dependent.");
|
|
6213
|
+
const where = point(index);
|
|
6214
|
+
const token = canonicalToken(tokenExpression);
|
|
6215
|
+
if (!token.value)
|
|
6216
|
+
return addOmission(index, "di_dynamic_token", token.reason ?? "Dynamic token.");
|
|
6217
|
+
resolutionCandidates.push({
|
|
6218
|
+
type: "resolution",
|
|
6219
|
+
token: token.value,
|
|
6220
|
+
framework,
|
|
6221
|
+
registry: receiver ? registryFor(framework, receiver, index) : `${framework}:decorator`,
|
|
6222
|
+
owner: ownerAt(file, where.line),
|
|
6223
|
+
file,
|
|
6224
|
+
line: where.line,
|
|
6225
|
+
column: where.column,
|
|
6226
|
+
evidence: source.split(/\r?\n/)[where.line - 1]?.trim().slice(0, 240) ?? "",
|
|
6227
|
+
});
|
|
6228
|
+
};
|
|
6229
|
+
if (hasInversify) {
|
|
6230
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.bind\s*\(([^()]+)\)\.to\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g))
|
|
6231
|
+
if (receivers.has(match[1]))
|
|
6232
|
+
addBinding(match.index ?? 0, match[2], match[3], "inversify", match[1]);
|
|
6233
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.bind\s*\(\s*([A-Za-z_$][\w$]*)\s*\)\.toSelf\s*\(\s*\)/g))
|
|
6234
|
+
if (receivers.has(match[1]))
|
|
6235
|
+
addBinding(match.index ?? 0, match[2], match[2], "inversify", match[1]);
|
|
6236
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.get\s*\(([^()]+)\)/g))
|
|
6237
|
+
if (receivers.has(match[1]))
|
|
6238
|
+
addResolution(match.index ?? 0, match[2], "inversify", match[1]);
|
|
6239
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.bind\s*\([^)]*\)\.toFactory\s*\(/g))
|
|
6240
|
+
if (receivers.has(match[1]))
|
|
6241
|
+
addOmission(match.index ?? 0, "di_unsupported_factory", "Factory providers are runtime behavior and are not resolved.");
|
|
6242
|
+
}
|
|
6243
|
+
if (hasTsyringe) {
|
|
6244
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.register\s*\(([^,()]+),\s*\{\s*useClass\s*:\s*([A-Za-z_$][\w$]*)\s*\}\s*\)/g))
|
|
6245
|
+
if (receivers.has(match[1]))
|
|
6246
|
+
addBinding(match.index ?? 0, match[2], match[3], "tsyringe", match[1]);
|
|
6247
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.registerSingleton\s*\(([^,()]+),\s*([A-Za-z_$][\w$]*)\s*\)/g))
|
|
6248
|
+
if (receivers.has(match[1]))
|
|
6249
|
+
addBinding(match.index ?? 0, match[2], match[3], "tsyringe", match[1]);
|
|
6250
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.registerSingleton\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g))
|
|
6251
|
+
if (receivers.has(match[1]))
|
|
6252
|
+
addBinding(match.index ?? 0, match[2], match[2], "tsyringe", match[1]);
|
|
6253
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.resolve\s*\(([^()]+)\)/g))
|
|
6254
|
+
if (receivers.has(match[1]))
|
|
6255
|
+
addResolution(match.index ?? 0, match[2], "tsyringe", match[1]);
|
|
6256
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.register\s*\(([^,()]+),\s*\{\s*useFactory\s*:/g))
|
|
6257
|
+
if (receivers.has(match[1]))
|
|
6258
|
+
addOmission(match.index ?? 0, "di_unsupported_factory", "Factory providers are runtime behavior and are not resolved.");
|
|
6259
|
+
}
|
|
6260
|
+
for (const [local, binding] of imports) {
|
|
6261
|
+
if (binding.original !== "inject" || !["inversify", "tsyringe"].includes(binding.module))
|
|
6262
|
+
continue;
|
|
6263
|
+
const escaped = local.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6264
|
+
for (const match of masked.matchAll(new RegExp(String.raw `@${escaped}\s*\(([^()]+)\)`, "g")))
|
|
6265
|
+
addResolution(match.index ?? 0, match[1], binding.module);
|
|
6266
|
+
}
|
|
6267
|
+
for (const match of masked.matchAll(/\b([A-Za-z_$][\w$]*)\.(bind|get|resolve|register|registerSingleton)\s*\(/g)) {
|
|
6268
|
+
if (!receivers.has(match[1]))
|
|
6269
|
+
addOmission(match.index ?? 0, "di_unverified_receiver", `Receiver ${match[1]} is not a source-proven framework container.`);
|
|
6270
|
+
}
|
|
6271
|
+
for (const receiver of receivers) {
|
|
6272
|
+
const escaped = receiver.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6273
|
+
for (const match of masked.matchAll(new RegExp(String.raw `\b${escaped}\.load\s*\(`, "g")))
|
|
6274
|
+
addOmission(match.index ?? 0, "di_runtime_module", "Runtime container modules are not statically expanded.");
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
const bindingsByToken = new Map();
|
|
6278
|
+
for (const binding of bindingCandidates) {
|
|
6279
|
+
const key = `${binding.framework}\0${binding.registry}\0${binding.token}`;
|
|
6280
|
+
const rows = bindingsByToken.get(key) ?? [];
|
|
6281
|
+
rows.push(binding);
|
|
6282
|
+
bindingsByToken.set(key, rows);
|
|
6283
|
+
}
|
|
6284
|
+
for (const [key, bindings] of bindingsByToken) {
|
|
6285
|
+
if (bindings.length === 1)
|
|
6286
|
+
candidates.push(bindings[0]);
|
|
6287
|
+
else {
|
|
6288
|
+
for (const binding of bindings) {
|
|
6289
|
+
candidates.push({
|
|
6290
|
+
...binding,
|
|
6291
|
+
type: "omission",
|
|
6292
|
+
kind: "di_ambiguous_binding",
|
|
6293
|
+
reason: `Registry token ${key.split("\0").at(-1)} has ${bindings.length} static bindings.`,
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
}
|
|
6298
|
+
for (const resolution of resolutionCandidates) {
|
|
6299
|
+
const bindings = resolution.registry?.endsWith(":decorator")
|
|
6300
|
+
? bindingCandidates.filter((binding) => binding.framework === resolution.framework && binding.token === resolution.token)
|
|
6301
|
+
: (bindingsByToken.get(`${resolution.framework}\0${resolution.registry}\0${resolution.token}`) ?? []);
|
|
6302
|
+
if (bindings.length === 1)
|
|
6303
|
+
candidates.push({ ...resolution, implementation: bindings[0].implementation });
|
|
6304
|
+
else
|
|
6305
|
+
candidates.push({
|
|
6306
|
+
...resolution,
|
|
6307
|
+
type: "omission",
|
|
6308
|
+
kind: bindings.length > 1 ? "di_ambiguous_resolution" : "di_unbound_resolution",
|
|
6309
|
+
reason: bindings.length > 1
|
|
6310
|
+
? `Token ${resolution.token} has multiple bindings.`
|
|
6311
|
+
: `Token ${resolution.token} has no static binding.`,
|
|
6312
|
+
});
|
|
6313
|
+
}
|
|
6314
|
+
db.run("BEGIN TRANSACTION;");
|
|
6315
|
+
try {
|
|
6316
|
+
db.run("DELETE FROM \"references\" WHERE kind IN ('di_binding', 'di_resolution', 'di_omission')");
|
|
6317
|
+
const insert = db.prepare(`
|
|
6318
|
+
INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind, confidence)
|
|
6319
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6320
|
+
`);
|
|
6321
|
+
for (const candidate of candidates) {
|
|
6322
|
+
if (!candidate.owner)
|
|
6323
|
+
continue;
|
|
6324
|
+
if (candidate.type === "omission") {
|
|
6325
|
+
insert.run(candidate.owner.name, candidate.file, `${candidate.kind}:${candidate.reason}`, null, candidate.line, candidate.column, "di_omission", 0);
|
|
6326
|
+
}
|
|
6327
|
+
else if (candidate.implementation) {
|
|
6328
|
+
insert.run(candidate.owner.name, candidate.file, candidate.implementation.name, candidate.implementation.filePath, candidate.line, candidate.column, candidate.type === "binding" ? "di_binding" : "di_resolution", 1);
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
6331
|
+
insert.finalize();
|
|
6332
|
+
db.run("COMMIT;");
|
|
6333
|
+
}
|
|
6334
|
+
catch (error) {
|
|
6335
|
+
db.run("ROLLBACK;");
|
|
6336
|
+
throw error;
|
|
6337
|
+
}
|
|
6338
|
+
persistReferenceEndpoints(db);
|
|
6339
|
+
setMeta(db, "typescriptDiVersion", "1");
|
|
6340
|
+
}
|
|
6341
|
+
/**
|
|
6342
|
+
* Decide whether a changed-file repair can affect the repository-wide DI
|
|
6343
|
+
* overlay without reading source outside that repair set. This check must run
|
|
6344
|
+
* before file-owned rows are replaced so removal of a framework import still
|
|
6345
|
+
* triggers reconciliation through the previously persisted DI evidence.
|
|
6346
|
+
*/
|
|
6347
|
+
function repairPathsAffectTypeScriptDi(db, repoPath, repairPaths) {
|
|
6348
|
+
const typeScriptPaths = repairPaths.filter((file) => (file.endsWith(".ts") || file.endsWith(".tsx")) && !file.endsWith(".d.ts"));
|
|
6349
|
+
if (typeScriptPaths.length === 0)
|
|
6350
|
+
return false;
|
|
6351
|
+
const persistedEvidence = db.query(`SELECT 1 AS present
|
|
6352
|
+
FROM symbols
|
|
6353
|
+
WHERE kind = 'di_module' AND filePath = ?
|
|
6354
|
+
UNION ALL
|
|
6355
|
+
SELECT 1 AS present
|
|
6356
|
+
FROM "references"
|
|
6357
|
+
WHERE kind IN ('di_binding', 'di_resolution', 'di_omission')
|
|
6358
|
+
AND (callerFile = ? OR calleeFile = ?)
|
|
6359
|
+
LIMIT 1`);
|
|
6360
|
+
if (typeScriptPaths.some((file) => persistedEvidence.get(file, file, file)))
|
|
6361
|
+
return true;
|
|
6362
|
+
return typeScriptPaths.some((file) => {
|
|
6363
|
+
try {
|
|
6364
|
+
const source = fs.readFileSync(path.join(repoPath, file), "utf8");
|
|
6365
|
+
const importsOnly = maskBlockComments(source, (text) => text.replace(/[^\n]/g, " ")).replace(/(^|\s)\/\/.*$/gm, (text, prefix) => prefix + " ".repeat(text.length - prefix.length));
|
|
6366
|
+
return /^\s*import\b[^\n]*\bfrom\s+["'](?:inversify|tsyringe)["']/m.test(importsOnly);
|
|
6367
|
+
}
|
|
6368
|
+
catch {
|
|
6369
|
+
return false;
|
|
6370
|
+
}
|
|
6371
|
+
});
|
|
6372
|
+
}
|
|
5876
6373
|
// The installed Transformers runtime was slower for every tested multi-item
|
|
5877
6374
|
// batch on the R43 clean-build protocol (R44). Keep the safe no-regression
|
|
5878
6375
|
// default while retaining the bounded override for future runtime/hardware
|
|
@@ -6584,6 +7081,7 @@ async function reconcileIndex(repoPath, db, progress) {
|
|
|
6584
7081
|
if (reindexed > 0) {
|
|
6585
7082
|
progress?.("finalizing", 0, "Refreshing identities and semantic embeddings");
|
|
6586
7083
|
persistSymbolIdentities(db, repoPath, reindexedPaths);
|
|
7084
|
+
reconcileTypeScriptDi(db, repoPath);
|
|
6587
7085
|
await indexEmbeddings(db, repoPath, progress);
|
|
6588
7086
|
indexGeneration++;
|
|
6589
7087
|
}
|
|
@@ -6997,6 +7495,7 @@ async function indexRepo(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
6997
7495
|
}
|
|
6998
7496
|
progress?.("finalizing", 0, "Persisting symbol identities and semantic index");
|
|
6999
7497
|
persistSymbolIdentities(db, repoPath);
|
|
7498
|
+
reconcileTypeScriptDi(db, repoPath);
|
|
7000
7499
|
// Structure is complete at this point; embeddings are the expensive tail. When
|
|
7001
7500
|
// deferred, `semanticReadiness` reports the gap until a later pass fills it —
|
|
7002
7501
|
// `indexEmbeddings` only ever embeds symbols that lack one, so resuming is
|
|
@@ -7080,6 +7579,7 @@ function startFileWatcher(repoPath, db) {
|
|
|
7080
7579
|
}
|
|
7081
7580
|
}
|
|
7082
7581
|
persistSymbolIdentities(db, resolvedRepoPath, paths);
|
|
7582
|
+
reconcileTypeScriptDi(db, resolvedRepoPath);
|
|
7083
7583
|
await indexEmbeddings(db, resolvedRepoPath);
|
|
7084
7584
|
queue.embeddingReconciliations++;
|
|
7085
7585
|
indexGeneration++;
|
|
@@ -7220,11 +7720,9 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
7220
7720
|
// newly indexable JSON files. v22 admits markdown as a link-only
|
|
7221
7721
|
// participant, so previously skipped documentation must be visited once
|
|
7222
7722
|
// to populate its doc_link / doc_link_broken edges.
|
|
7223
|
-
const KNODIN_SCHEMA_VERSION = 22;
|
|
7224
7723
|
// Highest version whose upgrade needs the stored data REBUILT. Versions
|
|
7225
7724
|
// above it migrate in place, so an upgrade costs a DELETE rather than a
|
|
7226
7725
|
// full re-index + re-embed (~40 min of CPU on an 18k-symbol corpus).
|
|
7227
|
-
const LAST_REBUILD_SCHEMA_VERSION = 22;
|
|
7228
7726
|
const versionRow = db.query("PRAGMA user_version").get();
|
|
7229
7727
|
const storedVersion = versionRow?.user_version ?? 0;
|
|
7230
7728
|
const needsMcpBackfill = storedVersion < 17;
|
|
@@ -7479,6 +7977,8 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
7479
7977
|
options.onProgress?.("finalizing", 0, `Persisting stable identities for ${missingIdentities.toLocaleString()} missing symbols`);
|
|
7480
7978
|
persistSymbolIdentities(db, normalizedPath);
|
|
7481
7979
|
}
|
|
7980
|
+
if (getMeta(db, "typescriptDiVersion") !== "1")
|
|
7981
|
+
reconcileTypeScriptDi(db, normalizedPath);
|
|
7482
7982
|
if (needsMcpBackfill || getMeta(db, "mcpBackfillVersion") !== "17") {
|
|
7483
7983
|
let backfillComplete = true;
|
|
7484
7984
|
const indexedFiles = db
|
|
@@ -8082,7 +8582,8 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8082
8582
|
SELECT *
|
|
8083
8583
|
FROM "references"
|
|
8084
8584
|
WHERE calleeSymbol = ? AND calleeFile = ?
|
|
8085
|
-
AND (? IS NULL OR calleeIdentity = ?)
|
|
8585
|
+
AND (? IS NULL OR calleeIdentity = ?)
|
|
8586
|
+
AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
8086
8587
|
`);
|
|
8087
8588
|
rows = stmt.all(targetSymbol, targetFile, targetIdentity, targetIdentity);
|
|
8088
8589
|
stmt.finalize();
|
|
@@ -8091,7 +8592,7 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8091
8592
|
const stmt = db.query(`
|
|
8092
8593
|
SELECT id, callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind
|
|
8093
8594
|
FROM "references"
|
|
8094
|
-
WHERE calleeSymbol = ? AND kind
|
|
8595
|
+
WHERE calleeSymbol = ? AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
8095
8596
|
`);
|
|
8096
8597
|
rows = stmt.all(targetSymbol);
|
|
8097
8598
|
stmt.finalize();
|
|
@@ -8108,6 +8609,9 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8108
8609
|
lineNumber: row.line,
|
|
8109
8610
|
repoPath: currentRepoPath,
|
|
8110
8611
|
kind: row.kind,
|
|
8612
|
+
confidence: row.confidence ?? 1,
|
|
8613
|
+
evidenceFile: row.callerFile,
|
|
8614
|
+
evidenceLine: row.line,
|
|
8111
8615
|
});
|
|
8112
8616
|
const parentCallers = findCallersFederated(allRepos, currentRepoPath, row.callerSymbol, depth - 1, visited, row.callerFile, row.callerIdentity ?? null);
|
|
8113
8617
|
results.push(...parentCallers);
|
|
@@ -8143,7 +8647,7 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8143
8647
|
// Deduplicate by symbol, file, line, and repoPath
|
|
8144
8648
|
const seen = new Set();
|
|
8145
8649
|
return results.filter((r) => {
|
|
8146
|
-
const key = `${r.symbol}:${r.filePath}:${r.lineNumber}:${r.repoPath}`;
|
|
8650
|
+
const key = `${r.symbol}:${r.filePath}:${r.lineNumber}:${r.repoPath}:${r.kind ?? "call"}`;
|
|
8147
8651
|
if (seen.has(key))
|
|
8148
8652
|
return false;
|
|
8149
8653
|
seen.add(key);
|
|
@@ -8181,7 +8685,8 @@ function findCalleesFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8181
8685
|
SELECT *
|
|
8182
8686
|
FROM "references"
|
|
8183
8687
|
WHERE callerSymbol = ? AND callerFile = ?
|
|
8184
|
-
AND (? IS NULL OR callerIdentity = ?)
|
|
8688
|
+
AND (? IS NULL OR callerIdentity = ?)
|
|
8689
|
+
AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
8185
8690
|
`);
|
|
8186
8691
|
rows = stmt.all(targetSymbol, sourceFile, exactSourceIdentity, exactSourceIdentity);
|
|
8187
8692
|
stmt.finalize();
|
|
@@ -8190,7 +8695,7 @@ function findCalleesFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8190
8695
|
const stmt = db.query(`
|
|
8191
8696
|
SELECT id, callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind
|
|
8192
8697
|
FROM "references"
|
|
8193
|
-
WHERE callerSymbol = ? AND kind
|
|
8698
|
+
WHERE callerSymbol = ? AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
8194
8699
|
`);
|
|
8195
8700
|
rows = stmt.all(targetSymbol);
|
|
8196
8701
|
stmt.finalize();
|
|
@@ -8218,6 +8723,9 @@ function findCalleesFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8218
8723
|
lineNumber: def.startLine,
|
|
8219
8724
|
repoPath: currentRepoPath,
|
|
8220
8725
|
kind: row.kind,
|
|
8726
|
+
confidence: row.confidence ?? 1,
|
|
8727
|
+
evidenceFile: row.callerFile,
|
|
8728
|
+
evidenceLine: row.line,
|
|
8221
8729
|
});
|
|
8222
8730
|
const childCallees = findCalleesFederated(allRepos, currentRepoPath, row.calleeSymbol, depth - 1, visited, def.filePath, def.identity ?? null);
|
|
8223
8731
|
results.push(...childCallees);
|
|
@@ -8260,7 +8768,7 @@ function findCalleesFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
8260
8768
|
// Deduplicate
|
|
8261
8769
|
const seen = new Set();
|
|
8262
8770
|
return results.filter((r) => {
|
|
8263
|
-
const key = `${r.symbol}:${r.filePath}:${r.lineNumber}:${r.repoPath}`;
|
|
8771
|
+
const key = `${r.symbol}:${r.filePath}:${r.lineNumber}:${r.repoPath}:${r.kind ?? "call"}`;
|
|
8264
8772
|
if (seen.has(key))
|
|
8265
8773
|
return false;
|
|
8266
8774
|
seen.add(key);
|
|
@@ -9714,24 +10222,28 @@ function findImportSpecifierLines(absPath, symbol) {
|
|
|
9714
10222
|
* threaded through each operation's many return sites, so the tag cannot be
|
|
9715
10223
|
* forgotten on an early return (a not-found explain, a refused rename).
|
|
9716
10224
|
*/
|
|
9717
|
-
function withStaleness(engine) {
|
|
10225
|
+
function withStaleness(engine, claimRepository) {
|
|
9718
10226
|
return {
|
|
9719
10227
|
async explain(symbol, repoPath, detailLevel, selector) {
|
|
10228
|
+
claimRepository(repoPath);
|
|
9720
10229
|
const result = await engine.explain(symbol, repoPath, detailLevel, selector);
|
|
9721
10230
|
result.staleness = stalenessFor(repoPath);
|
|
9722
10231
|
return result;
|
|
9723
10232
|
},
|
|
9724
10233
|
async review(base, repoPath, detailLevel, options) {
|
|
10234
|
+
claimRepository(repoPath);
|
|
9725
10235
|
const result = await engine.review(base, repoPath, detailLevel, options);
|
|
9726
10236
|
result.staleness = stalenessFor(repoPath);
|
|
9727
10237
|
return result;
|
|
9728
10238
|
},
|
|
9729
10239
|
async dependencyGraph(repoPath) {
|
|
10240
|
+
claimRepository(repoPath);
|
|
9730
10241
|
const result = await engine.dependencyGraph(repoPath);
|
|
9731
10242
|
result.staleness = stalenessFor(repoPath);
|
|
9732
10243
|
return result;
|
|
9733
10244
|
},
|
|
9734
10245
|
async map(repoPath, detailLevel = "minimal", options) {
|
|
10246
|
+
claimRepository(repoPath);
|
|
9735
10247
|
const raw = await engine.map(repoPath, detailLevel, options);
|
|
9736
10248
|
const result = options
|
|
9737
10249
|
? {
|
|
@@ -9775,21 +10287,26 @@ function withStaleness(engine) {
|
|
|
9775
10287
|
return result;
|
|
9776
10288
|
},
|
|
9777
10289
|
async wiki(repoPath, force) {
|
|
10290
|
+
claimRepository(repoPath);
|
|
9778
10291
|
const result = await engine.wiki(repoPath, force);
|
|
9779
10292
|
result.staleness = stalenessFor(repoPath);
|
|
9780
10293
|
return result;
|
|
9781
10294
|
},
|
|
9782
10295
|
status(repoPath, options) {
|
|
10296
|
+
claimRepository(repoPath);
|
|
9783
10297
|
return engine.status(repoPath, options);
|
|
9784
10298
|
},
|
|
9785
10299
|
repair(repoPath, options) {
|
|
10300
|
+
claimRepository(repoPath);
|
|
9786
10301
|
return engine.repair(repoPath, options);
|
|
9787
10302
|
},
|
|
9788
10303
|
index(repoPath, files, clean, options) {
|
|
10304
|
+
claimRepository(repoPath);
|
|
9789
10305
|
// `index` IS the refresh — it has no answer to qualify.
|
|
9790
10306
|
return engine.index(repoPath, files, clean, options);
|
|
9791
10307
|
},
|
|
9792
10308
|
async search(query, repoPath, limit, options) {
|
|
10309
|
+
claimRepository(repoPath);
|
|
9793
10310
|
const page = await engine.search(query, repoPath, limit, options);
|
|
9794
10311
|
const staleness = stalenessFor(repoPath);
|
|
9795
10312
|
for (const row of page.results)
|
|
@@ -9797,11 +10314,13 @@ function withStaleness(engine) {
|
|
|
9797
10314
|
return page;
|
|
9798
10315
|
},
|
|
9799
10316
|
async query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options) {
|
|
10317
|
+
claimRepository(repoPath);
|
|
9800
10318
|
const result = await engine.query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options);
|
|
9801
10319
|
result.staleness = stalenessFor(repoPath);
|
|
9802
10320
|
return result;
|
|
9803
10321
|
},
|
|
9804
10322
|
async rename(oldName, newName, repoPath, apply, verify, selector) {
|
|
10323
|
+
claimRepository(repoPath);
|
|
9805
10324
|
const result = await engine.rename(oldName, newName, repoPath, apply, verify, selector);
|
|
9806
10325
|
result.staleness = stalenessFor(repoPath);
|
|
9807
10326
|
return result;
|
|
@@ -9813,6 +10332,20 @@ function withStaleness(engine) {
|
|
|
9813
10332
|
}
|
|
9814
10333
|
/** Default SQLite + Tree-Sitter engine. */
|
|
9815
10334
|
export function createEngine() {
|
|
10335
|
+
const owner = Symbol("knodin-engine");
|
|
10336
|
+
const ownedRepositories = new Set();
|
|
10337
|
+
let closed = false;
|
|
10338
|
+
const claimRepository = (repoPath) => {
|
|
10339
|
+
if (closed)
|
|
10340
|
+
throw new Error("knodin engine is closed");
|
|
10341
|
+
const resolved = path.resolve(repoPath);
|
|
10342
|
+
if (ownedRepositories.has(resolved))
|
|
10343
|
+
return;
|
|
10344
|
+
ownedRepositories.add(resolved);
|
|
10345
|
+
const owners = repositoryEngineOwners.get(resolved) ?? new Set();
|
|
10346
|
+
owners.add(owner);
|
|
10347
|
+
repositoryEngineOwners.set(resolved, owners);
|
|
10348
|
+
};
|
|
9816
10349
|
// Bound to a name (rather than returned as a bare object literal) so the
|
|
9817
10350
|
// `wiki` method below can call `engine.map(..., "standard")` and reuse its output
|
|
9818
10351
|
// instead of duplicating community-detection logic. Safe: `wiki`'s body
|
|
@@ -9900,7 +10433,7 @@ export function createEngine() {
|
|
|
9900
10433
|
const out = [];
|
|
9901
10434
|
for (const c of refs) {
|
|
9902
10435
|
const filePath = formatPath(c.repoPath || targetRepoPath, c.filePath);
|
|
9903
|
-
const key = `${c.symbol}|${filePath}`;
|
|
10436
|
+
const key = `${c.symbol}|${filePath}|${c.kind ?? "call"}`;
|
|
9904
10437
|
if (seen.has(key))
|
|
9905
10438
|
continue;
|
|
9906
10439
|
seen.add(key);
|
|
@@ -9912,6 +10445,16 @@ export function createEngine() {
|
|
|
9912
10445
|
symbol: c.symbol,
|
|
9913
10446
|
filePath,
|
|
9914
10447
|
lineNumber: c.lineNumber,
|
|
10448
|
+
kind: c.kind,
|
|
10449
|
+
confidence: c.confidence,
|
|
10450
|
+
...(c.kind?.startsWith("di_")
|
|
10451
|
+
? {
|
|
10452
|
+
sourceEvidence: fs
|
|
10453
|
+
.readFileSync(path.join(c.repoPath || targetRepoPath, c.evidenceFile ?? c.filePath), "utf8")
|
|
10454
|
+
.split(/\r?\n/)[(c.evidenceLine ?? c.lineNumber) - 1]?.trim()
|
|
10455
|
+
.slice(0, 240) ?? "",
|
|
10456
|
+
}
|
|
10457
|
+
: {}),
|
|
9915
10458
|
...(definition
|
|
9916
10459
|
? {
|
|
9917
10460
|
identity: symbolIdentity(definitionRepo?.path || targetRepoPath, definition),
|
|
@@ -9923,11 +10466,39 @@ export function createEngine() {
|
|
|
9923
10466
|
};
|
|
9924
10467
|
const allCallers = dedupeRefs(directCallers);
|
|
9925
10468
|
const allCallees = dedupeRefs(directCallees);
|
|
10469
|
+
const targetDb = allRepos.find((repo) => repo.path === targetRepoPath)?.db;
|
|
10470
|
+
const omissions = targetDb
|
|
10471
|
+
? targetDb
|
|
10472
|
+
.query(" SELECT calleeSymbol, callerFile, line FROM \"references\" WHERE callerSymbol = ? AND callerFile = ? AND kind = 'di_omission' ORDER BY line, calleeSymbol")
|
|
10473
|
+
.all(primaryDef.name, primaryDef.filePath)
|
|
10474
|
+
.map((row) => {
|
|
10475
|
+
const separator = row.calleeSymbol.indexOf(":");
|
|
10476
|
+
let evidence = "";
|
|
10477
|
+
try {
|
|
10478
|
+
evidence =
|
|
10479
|
+
fs
|
|
10480
|
+
.readFileSync(path.join(targetRepoPath, row.callerFile), "utf8")
|
|
10481
|
+
.split(/\r?\n/)[row.line - 1]?.trim()
|
|
10482
|
+
.slice(0, 240) ?? "";
|
|
10483
|
+
}
|
|
10484
|
+
catch { }
|
|
10485
|
+
return {
|
|
10486
|
+
kind: separator < 0 ? "di_unresolved" : row.calleeSymbol.slice(0, separator),
|
|
10487
|
+
reason: separator < 0 ? row.calleeSymbol : row.calleeSymbol.slice(separator + 1),
|
|
10488
|
+
filePath: formatPath(targetRepoPath, row.callerFile),
|
|
10489
|
+
lineNumber: row.line,
|
|
10490
|
+
evidence,
|
|
10491
|
+
};
|
|
10492
|
+
})
|
|
10493
|
+
: [];
|
|
9926
10494
|
const allBlastFiles = Array.from(new Set(allCallers.map((c) => c.filePath)));
|
|
9927
10495
|
const untested = !allCallers.some((c) => isTestFilePath(c.filePath));
|
|
9928
10496
|
const minimal = detailLevel === "minimal";
|
|
9929
10497
|
const cap = minimal ? 25 : 200;
|
|
9930
|
-
const truncated = allCallers.length > cap ||
|
|
10498
|
+
const truncated = allCallers.length > cap ||
|
|
10499
|
+
allCallees.length > cap ||
|
|
10500
|
+
allBlastFiles.length > cap ||
|
|
10501
|
+
omissions.length > cap;
|
|
9931
10502
|
return {
|
|
9932
10503
|
identity: symbolIdentity(targetRepoPath, primaryDef),
|
|
9933
10504
|
symbol,
|
|
@@ -9937,6 +10508,9 @@ export function createEngine() {
|
|
|
9937
10508
|
callerCount: allCallers.length,
|
|
9938
10509
|
callees: allCallees.slice(0, cap),
|
|
9939
10510
|
calleeCount: allCallees.length,
|
|
10511
|
+
...(omissions.length > 0
|
|
10512
|
+
? { omissions: omissions.slice(0, cap), omissionCount: omissions.length }
|
|
10513
|
+
: {}),
|
|
9940
10514
|
blastRadius: allBlastFiles.slice(0, cap),
|
|
9941
10515
|
blastRadiusCount: allBlastFiles.length,
|
|
9942
10516
|
untested,
|
|
@@ -10350,7 +10924,7 @@ export function createEngine() {
|
|
|
10350
10924
|
addExtractedEdge(df.fromFile, df.toFile, df.kind, df.confidence, df.sourceEvidence);
|
|
10351
10925
|
}
|
|
10352
10926
|
const refsAll = db
|
|
10353
|
-
.query('SELECT callerFile, calleeSymbol, calleeFile FROM "references"')
|
|
10927
|
+
.query('SELECT callerFile, calleeSymbol, calleeFile, kind, line, confidence FROM "references"')
|
|
10354
10928
|
.all();
|
|
10355
10929
|
for (const r of refsAll) {
|
|
10356
10930
|
// Prefer the resolved calleeFile (one exact edge). When unresolved,
|
|
@@ -10364,7 +10938,12 @@ export function createEngine() {
|
|
|
10364
10938
|
}
|
|
10365
10939
|
if (!calleeFile || calleeFile === r.callerFile)
|
|
10366
10940
|
continue;
|
|
10367
|
-
addExtractedEdge(r.callerFile, calleeFile, "calls")
|
|
10941
|
+
addExtractedEdge(r.callerFile, calleeFile, r.kind.startsWith("di_") ? r.kind : "calls", r.confidence, r.kind.startsWith("di_")
|
|
10942
|
+
? fs
|
|
10943
|
+
.readFileSync(path.join(resolvedRepoPath, r.callerFile), "utf8")
|
|
10944
|
+
.split(/\r?\n/)[r.line - 1]?.trim()
|
|
10945
|
+
.slice(0, 240)
|
|
10946
|
+
: undefined);
|
|
10368
10947
|
}
|
|
10369
10948
|
return {
|
|
10370
10949
|
repoPath: resolvedRepoPath,
|
|
@@ -10458,7 +11037,7 @@ export function createEngine() {
|
|
|
10458
11037
|
// Populating callers references & cross-repo routing
|
|
10459
11038
|
for (const repo of allRepos) {
|
|
10460
11039
|
const refsAll = repo.db
|
|
10461
|
-
.query('SELECT callerFile, calleeSymbol, calleeFile FROM "references"')
|
|
11040
|
+
.query('SELECT callerFile, calleeSymbol, calleeFile, kind, line, confidence FROM "references"')
|
|
10462
11041
|
.all();
|
|
10463
11042
|
for (const r of refsAll) {
|
|
10464
11043
|
const callerPrefixed = formatPath(repo.path, r.callerFile);
|
|
@@ -10476,7 +11055,12 @@ export function createEngine() {
|
|
|
10476
11055
|
calleePrefixed = sameRepo[0].file;
|
|
10477
11056
|
}
|
|
10478
11057
|
if (calleePrefixed && calleePrefixed !== callerPrefixed) {
|
|
10479
|
-
addExtractedEdge(callerPrefixed, calleePrefixed, "calls")
|
|
11058
|
+
addExtractedEdge(callerPrefixed, calleePrefixed, r.kind.startsWith("di_") ? r.kind : "calls", r.confidence, r.kind.startsWith("di_")
|
|
11059
|
+
? fs
|
|
11060
|
+
.readFileSync(path.join(repo.path, r.callerFile), "utf8")
|
|
11061
|
+
.split(/\r?\n/)[r.line - 1]?.trim()
|
|
11062
|
+
.slice(0, 240)
|
|
11063
|
+
: undefined);
|
|
10480
11064
|
}
|
|
10481
11065
|
// 2. Cross-repo Routing: If it does not exist locally but matches endpoints in other repos
|
|
10482
11066
|
const localExists = calleeFiles?.some((c) => c.repoPath === repo.path);
|
|
@@ -10757,6 +11341,12 @@ export function createEngine() {
|
|
|
10757
11341
|
schemaProblems.push(`required index_state column is missing: ${column}`);
|
|
10758
11342
|
if (schemaVersion < 18)
|
|
10759
11343
|
schemaProblems.push(`schema ${schemaVersion} is older than required schema 18`);
|
|
11344
|
+
if (schemaVersion < LAST_REBUILD_SCHEMA_VERSION)
|
|
11345
|
+
schemaProblems.push(`schema ${schemaVersion} requires a full rebuild for current schema ${KNODIN_SCHEMA_VERSION}`);
|
|
11346
|
+
else if (schemaVersion < KNODIN_SCHEMA_VERSION)
|
|
11347
|
+
schemaProblems.push(`schema ${schemaVersion} requires migration to current schema ${KNODIN_SCHEMA_VERSION}`);
|
|
11348
|
+
else if (schemaVersion > KNODIN_SCHEMA_VERSION)
|
|
11349
|
+
schemaProblems.push(`schema ${schemaVersion} is newer than supported schema ${KNODIN_SCHEMA_VERSION}`);
|
|
10760
11350
|
if (schemaProblems.length) {
|
|
10761
11351
|
if (!activeDb)
|
|
10762
11352
|
db.close();
|
|
@@ -10783,10 +11373,15 @@ export function createEngine() {
|
|
|
10783
11373
|
lastIndexedHead: "",
|
|
10784
11374
|
freshness: buildFreshnessEnvelope(resolved, db, verifiedAt, "repair-needed"),
|
|
10785
11375
|
verification: { mode: "deep-audit", verifiedAt },
|
|
10786
|
-
repairSteps:
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
11376
|
+
repairSteps: schemaVersion > KNODIN_SCHEMA_VERSION
|
|
11377
|
+
? [
|
|
11378
|
+
"Upgrade knodin to a version that supports this local graph schema.",
|
|
11379
|
+
"Run `knodin status` again after upgrading.",
|
|
11380
|
+
]
|
|
11381
|
+
: [
|
|
11382
|
+
"Run `knodin repair` to migrate/rebuild damaged local schema.",
|
|
11383
|
+
"Run `knodin status` again to verify health.",
|
|
11384
|
+
],
|
|
10790
11385
|
};
|
|
10791
11386
|
}
|
|
10792
11387
|
const indexedRows = db
|
|
@@ -10908,6 +11503,7 @@ export function createEngine() {
|
|
|
10908
11503
|
options?.signal?.removeEventListener("abort", abortShared);
|
|
10909
11504
|
}
|
|
10910
11505
|
}
|
|
11506
|
+
invalidateResolutionCaches(resolved);
|
|
10911
11507
|
const operationId = crypto.randomUUID();
|
|
10912
11508
|
const startedAt = Date.now();
|
|
10913
11509
|
let sequence = 0;
|
|
@@ -10978,6 +11574,23 @@ export function createEngine() {
|
|
|
10978
11574
|
phaseTotal: 1,
|
|
10979
11575
|
});
|
|
10980
11576
|
cancelBoundary();
|
|
11577
|
+
if (before.schemaVersion > KNODIN_SCHEMA_VERSION) {
|
|
11578
|
+
const outstandingIssues = {
|
|
11579
|
+
files: before.missing.files.length,
|
|
11580
|
+
records: before.missing.records.length,
|
|
11581
|
+
total: before.missing.files.length + before.missing.records.length,
|
|
11582
|
+
};
|
|
11583
|
+
emitProgress("completed", overallCompleted, "Repair stopped because the local graph schema requires a newer knodin version");
|
|
11584
|
+
return {
|
|
11585
|
+
repaired,
|
|
11586
|
+
before,
|
|
11587
|
+
after: before,
|
|
11588
|
+
verified: false,
|
|
11589
|
+
cancelled: false,
|
|
11590
|
+
remaining: 0,
|
|
11591
|
+
outstandingIssues,
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
10981
11594
|
emitProgress("planning", 0, "Building the repair plan");
|
|
10982
11595
|
const repairKinds = new Map();
|
|
10983
11596
|
for (const file of before.missing.files)
|
|
@@ -11028,11 +11641,15 @@ export function createEngine() {
|
|
|
11028
11641
|
skipContentReconciliation: true,
|
|
11029
11642
|
skipFreshnessGuard: true,
|
|
11030
11643
|
}));
|
|
11644
|
+
const schemaPrepared = before.schemaVersion !== KNODIN_SCHEMA_VERSION;
|
|
11645
|
+
if (schemaPrepared)
|
|
11646
|
+
committedWork = true;
|
|
11031
11647
|
const inheritedPostprocessing = getMeta(db, "repairPostprocessingPending") === "1";
|
|
11032
11648
|
const eligibleRepairPaths = new Set(collectRepoFiles(resolved));
|
|
11033
11649
|
repairPaths = [...repairKinds.keys()]
|
|
11034
11650
|
.filter((file) => repairKinds.get(file) === "stale" || fileDriftedFromIndexState(db, resolved, file))
|
|
11035
11651
|
.sort(compareBytes);
|
|
11652
|
+
const repairAffectsTypeScriptDi = inheritedPostprocessing || repairPathsAffectTypeScriptDi(db, resolved, repairPaths);
|
|
11036
11653
|
counts.skipped = Math.max(0, repairKinds.size - repairPaths.length);
|
|
11037
11654
|
overallTotal = repairPaths.length + 4;
|
|
11038
11655
|
emitProgress("planning", repairPaths.length, "Repair plan completed", {
|
|
@@ -11098,6 +11715,8 @@ export function createEngine() {
|
|
|
11098
11715
|
});
|
|
11099
11716
|
cancelBoundary();
|
|
11100
11717
|
persistSymbolIdentities(db, resolved, inheritedPostprocessing ? undefined : repairPaths);
|
|
11718
|
+
if (repairAffectsTypeScriptDi)
|
|
11719
|
+
reconcileTypeScriptDi(db, resolved);
|
|
11101
11720
|
committedWork = true;
|
|
11102
11721
|
overallCompleted++;
|
|
11103
11722
|
emitProgress("symbol-identities", 1, "Stable symbol identities persisted", {
|
|
@@ -11181,8 +11800,7 @@ export function createEngine() {
|
|
|
11181
11800
|
phaseTotal: 1,
|
|
11182
11801
|
});
|
|
11183
11802
|
cancelBoundary();
|
|
11184
|
-
if (repairKinds.size > 0 || hasOrphans || needsPostprocessing) {
|
|
11185
|
-
const reconciledAt = new Date().toISOString();
|
|
11803
|
+
if (schemaPrepared || repairKinds.size > 0 || hasOrphans || needsPostprocessing) {
|
|
11186
11804
|
// The first deep audit proves graph-content integrity. Only
|
|
11187
11805
|
// after that proof may this snapshot claim the current
|
|
11188
11806
|
// revision; a second deep audit proves the complete envelope.
|
|
@@ -11190,15 +11808,38 @@ export function createEngine() {
|
|
|
11190
11808
|
after.missing.files.length === 0 &&
|
|
11191
11809
|
after.missing.records.length === 0 &&
|
|
11192
11810
|
Object.values(after.orphaned).every((count) => count === 0);
|
|
11193
|
-
if (contentVerified
|
|
11194
|
-
(
|
|
11195
|
-
|
|
11811
|
+
if (contentVerified) {
|
|
11812
|
+
db.run("BEGIN TRANSACTION;");
|
|
11813
|
+
try {
|
|
11814
|
+
if (after.freshness.state === "stale-head" || after.freshness.state === "unknown") {
|
|
11815
|
+
setMeta(db, "lastIndexedHead", gitHead(resolved) ?? "");
|
|
11816
|
+
}
|
|
11817
|
+
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
11818
|
+
recordFreshnessBaseline(resolved, db);
|
|
11819
|
+
statusCache.delete(resolved);
|
|
11820
|
+
const candidate = await engine.status(resolved, { audit: "deep" });
|
|
11821
|
+
if (candidate.status === "healthy") {
|
|
11822
|
+
db.run("COMMIT;");
|
|
11823
|
+
after = candidate;
|
|
11824
|
+
}
|
|
11825
|
+
else {
|
|
11826
|
+
db.run("ROLLBACK;");
|
|
11827
|
+
statusCache.delete(resolved);
|
|
11828
|
+
after = await engine.status(resolved, { audit: "deep" });
|
|
11829
|
+
}
|
|
11830
|
+
}
|
|
11831
|
+
catch (error) {
|
|
11832
|
+
db.run("ROLLBACK;");
|
|
11833
|
+
statusCache.delete(resolved);
|
|
11834
|
+
throw error;
|
|
11835
|
+
}
|
|
11196
11836
|
}
|
|
11197
|
-
setMeta(db, "lastSuccessfulReconciliation", reconciledAt);
|
|
11198
|
-
recordFreshnessBaseline(resolved, db);
|
|
11199
|
-
statusCache.delete(resolved);
|
|
11200
|
-
after = await engine.status(resolved, { audit: "deep" });
|
|
11201
11837
|
}
|
|
11838
|
+
const outstandingIssues = {
|
|
11839
|
+
files: after.missing.files.length,
|
|
11840
|
+
records: after.missing.records.length,
|
|
11841
|
+
total: after.missing.files.length + after.missing.records.length,
|
|
11842
|
+
};
|
|
11202
11843
|
const result = {
|
|
11203
11844
|
repaired,
|
|
11204
11845
|
before,
|
|
@@ -11206,6 +11847,7 @@ export function createEngine() {
|
|
|
11206
11847
|
verified: after.status === "healthy",
|
|
11207
11848
|
cancelled: false,
|
|
11208
11849
|
remaining: 0,
|
|
11850
|
+
outstandingIssues,
|
|
11209
11851
|
};
|
|
11210
11852
|
emitProgress("completed", overallCompleted, result.verified
|
|
11211
11853
|
? "Repair completed and verified"
|
|
@@ -11226,6 +11868,11 @@ export function createEngine() {
|
|
|
11226
11868
|
verified: false,
|
|
11227
11869
|
cancelled: true,
|
|
11228
11870
|
remaining,
|
|
11871
|
+
outstandingIssues: {
|
|
11872
|
+
files: after.missing.files.length,
|
|
11873
|
+
records: after.missing.records.length,
|
|
11874
|
+
total: after.missing.files.length + after.missing.records.length,
|
|
11875
|
+
},
|
|
11229
11876
|
};
|
|
11230
11877
|
}
|
|
11231
11878
|
counts.failed++;
|
|
@@ -11249,6 +11896,7 @@ export function createEngine() {
|
|
|
11249
11896
|
}
|
|
11250
11897
|
},
|
|
11251
11898
|
async index(repoPath, files, clean = false, options) {
|
|
11899
|
+
invalidateResolutionCaches(repoPath);
|
|
11252
11900
|
const indexed = [];
|
|
11253
11901
|
const unchanged = [];
|
|
11254
11902
|
let scipReport;
|
|
@@ -11299,9 +11947,14 @@ export function createEngine() {
|
|
|
11299
11947
|
indexed.push(relativePath);
|
|
11300
11948
|
progress("indexing-files", fileIndex + 1, `Checking ${files.length.toLocaleString()} requested files`, { phaseTotal: files.length });
|
|
11301
11949
|
}
|
|
11950
|
+
const refreshedFlows = await refreshFlowsForApexFiles(db, repoPath, indexed);
|
|
11951
|
+
for (const flow of refreshedFlows)
|
|
11952
|
+
if (!indexed.includes(flow))
|
|
11953
|
+
indexed.push(flow);
|
|
11302
11954
|
if (indexed.length > 0) {
|
|
11303
11955
|
progress("finalizing", 0, "Refreshing identities and semantic embeddings");
|
|
11304
11956
|
persistSymbolIdentities(db, repoPath, indexed);
|
|
11957
|
+
reconcileTypeScriptDi(db, repoPath);
|
|
11305
11958
|
await indexEmbeddings(db, repoPath, progress);
|
|
11306
11959
|
indexGeneration++;
|
|
11307
11960
|
}
|
|
@@ -11961,6 +12614,14 @@ export function createEngine() {
|
|
|
11961
12614
|
throw new Error(`knodin query ${pattern}: includeDataFlow is unsupported`);
|
|
11962
12615
|
if (options.flowVariable !== undefined && pattern !== "flow_analysis")
|
|
11963
12616
|
throw new Error(`knodin query ${pattern}: flowVariable is unsupported`);
|
|
12617
|
+
if ((options.resourceOffset !== undefined ||
|
|
12618
|
+
options.resourceMaxBytes !== undefined ||
|
|
12619
|
+
options.resourceMaxTokens !== undefined) &&
|
|
12620
|
+
pattern !== "resource_reachability")
|
|
12621
|
+
throw new Error(`knodin query ${pattern}: resource reachability bounds are unsupported`);
|
|
12622
|
+
if (options.resourceOffset !== undefined &&
|
|
12623
|
+
(!Number.isInteger(options.resourceOffset) || options.resourceOffset < 0))
|
|
12624
|
+
throw new Error("knodin query resource_reachability: offset must be a non-negative integer");
|
|
11964
12625
|
if (options.architectureFacets?.length && pattern !== "architecture_overview")
|
|
11965
12626
|
throw new Error(`knodin query ${pattern}: architectureFacets is unsupported`);
|
|
11966
12627
|
if (options.architectureFacets?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet)))
|
|
@@ -11978,6 +12639,7 @@ export function createEngine() {
|
|
|
11978
12639
|
"structural_implementations_of",
|
|
11979
12640
|
"tests_for",
|
|
11980
12641
|
"shortest_path",
|
|
12642
|
+
"cross_substrate_path",
|
|
11981
12643
|
"rename_preview",
|
|
11982
12644
|
"traverse",
|
|
11983
12645
|
"feature_path",
|
|
@@ -11995,6 +12657,22 @@ export function createEngine() {
|
|
|
11995
12657
|
count: 0,
|
|
11996
12658
|
results: [],
|
|
11997
12659
|
...(pattern === "shortest_path" ? { path: [] } : {}),
|
|
12660
|
+
...(pattern === "cross_substrate_path"
|
|
12661
|
+
? {
|
|
12662
|
+
crossSubstratePath: {
|
|
12663
|
+
steps: [],
|
|
12664
|
+
omissions: [
|
|
12665
|
+
"The source endpoint is ambiguous; select a stable identity or file.",
|
|
12666
|
+
],
|
|
12667
|
+
unsupportedCrossings: [
|
|
12668
|
+
"Terraform, dbt, and arbitrary Salesforce metadata crossings are not resolved.",
|
|
12669
|
+
],
|
|
12670
|
+
budget: { stepLimit: 1, returnedSteps: 0 },
|
|
12671
|
+
truncated: false,
|
|
12672
|
+
continuation: null,
|
|
12673
|
+
},
|
|
12674
|
+
}
|
|
12675
|
+
: {}),
|
|
11998
12676
|
ambiguity: resolved.ambiguity,
|
|
11999
12677
|
};
|
|
12000
12678
|
selectedTarget = resolved.selected;
|
|
@@ -12877,6 +13555,102 @@ export function createEngine() {
|
|
|
12877
13555
|
},
|
|
12878
13556
|
})));
|
|
12879
13557
|
}
|
|
13558
|
+
case "cross_substrate_path": {
|
|
13559
|
+
const unsupportedCrossings = [
|
|
13560
|
+
"Terraform, dbt, and arbitrary Salesforce metadata crossings are not resolved.",
|
|
13561
|
+
];
|
|
13562
|
+
const empty = (omission) => finish([], {
|
|
13563
|
+
crossSubstratePath: {
|
|
13564
|
+
steps: [],
|
|
13565
|
+
omissions: [omission],
|
|
13566
|
+
unsupportedCrossings,
|
|
13567
|
+
budget: { stepLimit: 1, returnedSteps: 0 },
|
|
13568
|
+
truncated: false,
|
|
13569
|
+
continuation: null,
|
|
13570
|
+
},
|
|
13571
|
+
});
|
|
13572
|
+
if (!to)
|
|
13573
|
+
return empty("Both from and to endpoints are required.");
|
|
13574
|
+
if (!selectedTarget)
|
|
13575
|
+
return empty("The source endpoint was not resolved uniquely.");
|
|
13576
|
+
const destination = resolveSymbolRows(db, primary.path, to, {
|
|
13577
|
+
identity: selector.toIdentity,
|
|
13578
|
+
file: selector.toFile,
|
|
13579
|
+
kind: selector.toKind,
|
|
13580
|
+
});
|
|
13581
|
+
if (destination.ambiguity)
|
|
13582
|
+
return finish([], {
|
|
13583
|
+
ambiguity: destination.ambiguity,
|
|
13584
|
+
crossSubstratePath: {
|
|
13585
|
+
steps: [],
|
|
13586
|
+
omissions: [
|
|
13587
|
+
"The destination endpoint is ambiguous; select a stable identity or file.",
|
|
13588
|
+
],
|
|
13589
|
+
unsupportedCrossings,
|
|
13590
|
+
budget: { stepLimit: 1, returnedSteps: 0 },
|
|
13591
|
+
truncated: false,
|
|
13592
|
+
continuation: null,
|
|
13593
|
+
},
|
|
13594
|
+
});
|
|
13595
|
+
if (!destination.selected)
|
|
13596
|
+
return empty("The destination endpoint was not resolved uniquely.");
|
|
13597
|
+
if (selectedTarget.kind !== "salesforce-metadata" ||
|
|
13598
|
+
!selectedTarget.filePath.endsWith(".flow-meta.xml") ||
|
|
13599
|
+
!destination.selected.filePath.endsWith(".cls"))
|
|
13600
|
+
return empty("The endpoints are not a supported Salesforce Flow-to-Apex boundary.");
|
|
13601
|
+
const className = path.basename(destination.selected.filePath, ".cls");
|
|
13602
|
+
const actionName = `${className}.${destination.selected.name}`;
|
|
13603
|
+
let flowEvidence = "";
|
|
13604
|
+
try {
|
|
13605
|
+
const flowSource = fs.readFileSync(path.join(primary.path, selectedTarget.filePath), "utf8");
|
|
13606
|
+
const matchingActions = [
|
|
13607
|
+
...flowSource.matchAll(/<actionCalls>([\s\S]*?)<\/actionCalls>/g),
|
|
13608
|
+
]
|
|
13609
|
+
.filter((match) => /<actionType>apex<\/actionType>/.test(match[1]))
|
|
13610
|
+
.filter((match) => staticXmlTagValues(match[1], "actionName").some((entry) => entry.value === actionName));
|
|
13611
|
+
if (matchingActions.length === 1)
|
|
13612
|
+
flowEvidence = matchingActions[0][0].trim();
|
|
13613
|
+
}
|
|
13614
|
+
catch { }
|
|
13615
|
+
const apexEvidence = findUniqueInvocableApexMethod(className, destination.selected.name, primary.path)
|
|
13616
|
+
?.evidence ?? "";
|
|
13617
|
+
if (!flowEvidence)
|
|
13618
|
+
return empty("No exact literal Apex action for the selected method exists in the Flow.");
|
|
13619
|
+
if (!apexEvidence)
|
|
13620
|
+
return empty("The selected Apex method is not one unique local @InvocableMethod.");
|
|
13621
|
+
if (!selectedTarget.identity || !destination.selected.identity)
|
|
13622
|
+
return empty("Exact endpoint identity is unavailable.");
|
|
13623
|
+
return finish([], {
|
|
13624
|
+
crossSubstratePath: {
|
|
13625
|
+
steps: [
|
|
13626
|
+
{
|
|
13627
|
+
kind: "flow_apex_action",
|
|
13628
|
+
from: {
|
|
13629
|
+
symbol: selectedTarget.name,
|
|
13630
|
+
file: selectedTarget.filePath,
|
|
13631
|
+
identity: symbolIdentity(primary.path, selectedTarget),
|
|
13632
|
+
substrate: "salesforce-flow",
|
|
13633
|
+
},
|
|
13634
|
+
to: {
|
|
13635
|
+
symbol: destination.selected.name,
|
|
13636
|
+
file: destination.selected.filePath,
|
|
13637
|
+
identity: symbolIdentity(primary.path, destination.selected),
|
|
13638
|
+
substrate: "salesforce-apex",
|
|
13639
|
+
},
|
|
13640
|
+
provenance: "EXTRACTED",
|
|
13641
|
+
confidence: 1,
|
|
13642
|
+
freshness: stalenessFor(resolvedRepoPath),
|
|
13643
|
+
evidence: { flow: flowEvidence, apex: apexEvidence },
|
|
13644
|
+
},
|
|
13645
|
+
],
|
|
13646
|
+
omissions: [],
|
|
13647
|
+
unsupportedCrossings,
|
|
13648
|
+
budget: { stepLimit: 1, returnedSteps: 1 },
|
|
13649
|
+
truncated: false,
|
|
13650
|
+
continuation: null,
|
|
13651
|
+
},
|
|
13652
|
+
});
|
|
13653
|
+
}
|
|
12880
13654
|
case "shortest_path": {
|
|
12881
13655
|
if (!to)
|
|
12882
13656
|
return finish([], { path: [] });
|
|
@@ -12890,7 +13664,7 @@ export function createEngine() {
|
|
|
12890
13664
|
return finish([], { path: [], ambiguity: destination.ambiguity });
|
|
12891
13665
|
if (selectedTarget && destination.selected) {
|
|
12892
13666
|
const edges = db
|
|
12893
|
-
.query("SELECT callerSymbol, callerFile, calleeSymbol, calleeFile, callerIdentity, calleeIdentity FROM \"references\" WHERE callerSymbol IS NOT NULL AND calleeFile IS NOT NULL AND kind
|
|
13667
|
+
.query("SELECT callerSymbol, callerFile, calleeSymbol, calleeFile, callerIdentity, calleeIdentity, kind FROM \"references\" WHERE callerSymbol IS NOT NULL AND calleeFile IS NOT NULL AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')")
|
|
12894
13668
|
.all();
|
|
12895
13669
|
const key = (name, file, identity) => `${name}\0${file}\0${identity ?? ""}`;
|
|
12896
13670
|
const start = key(selectedTarget.name, selectedTarget.filePath, selectedTarget.identity);
|
|
@@ -13339,6 +14113,57 @@ export function createEngine() {
|
|
|
13339
14113
|
...(truncated ? { truncated: true } : {}),
|
|
13340
14114
|
});
|
|
13341
14115
|
}
|
|
14116
|
+
case "resource_reachability": {
|
|
14117
|
+
const indexed = db
|
|
14118
|
+
.query("SELECT filePath FROM index_state ORDER BY filePath")
|
|
14119
|
+
.all()
|
|
14120
|
+
.filter(({ filePath }) => /\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(filePath));
|
|
14121
|
+
let diskDrifted = indexed.some(({ filePath }) => fileDriftedFromIndexState(db, resolvedRepoPath, filePath));
|
|
14122
|
+
const cachedCorpus = resourceCorpusCache.get(resolvedRepoPath);
|
|
14123
|
+
let inputs;
|
|
14124
|
+
let diskFingerprint;
|
|
14125
|
+
if (!diskDrifted && cachedCorpus?.generation === indexGeneration) {
|
|
14126
|
+
inputs = cachedCorpus.inputs;
|
|
14127
|
+
diskFingerprint = cachedCorpus.fingerprint;
|
|
14128
|
+
}
|
|
14129
|
+
else {
|
|
14130
|
+
inputs = indexed.flatMap(({ filePath }) => {
|
|
14131
|
+
const absolute = path.join(resolvedRepoPath, filePath);
|
|
14132
|
+
try {
|
|
14133
|
+
const content = fs.readFileSync(absolute, "utf8");
|
|
14134
|
+
if (fileDriftedFromIndexState(db, resolvedRepoPath, filePath))
|
|
14135
|
+
diskDrifted = true;
|
|
14136
|
+
return [{ path: filePath, content }];
|
|
14137
|
+
}
|
|
14138
|
+
catch {
|
|
14139
|
+
diskDrifted = true;
|
|
14140
|
+
return [];
|
|
14141
|
+
}
|
|
14142
|
+
});
|
|
14143
|
+
diskFingerprint = resourceFingerprint(inputs);
|
|
14144
|
+
if (!diskDrifted)
|
|
14145
|
+
setBoundedCache(resourceCorpusCache, resolvedRepoPath, { generation: indexGeneration, inputs, fingerprint: diskFingerprint }, RESOURCE_CORPUS_CACHE_LIMIT);
|
|
14146
|
+
}
|
|
14147
|
+
// Disk state is part of the cache identity. A generation-only key can
|
|
14148
|
+
// return a previously fresh result during the freshness-probe TTL after
|
|
14149
|
+
// an external edit. index_state is the graph's persisted disk snapshot;
|
|
14150
|
+
// any mismatch is passed into the analyzer as a fail-closed expectation.
|
|
14151
|
+
const cacheKey = `${resolvedRepoPath}\0${diskFingerprint}\0${diskDrifted ? "drifted" : "matched"}\0${cap}\0${options.resourceOffset ?? 0}\0${options.resourceMaxBytes ?? 65_536}\0${options.resourceMaxTokens ?? 16_384}`;
|
|
14152
|
+
const cached = resourceReachabilityCache.get(cacheKey);
|
|
14153
|
+
if (cached?.generation === indexGeneration) {
|
|
14154
|
+
const result = structuredClone(cached.result);
|
|
14155
|
+
return finish([], { count: result.paths.length, resourceReachability: result });
|
|
14156
|
+
}
|
|
14157
|
+
const result = analyzeResourceReachability(inputs, {
|
|
14158
|
+
maxItems: cap,
|
|
14159
|
+
maxBytes: options.resourceMaxBytes,
|
|
14160
|
+
maxTokens: options.resourceMaxTokens,
|
|
14161
|
+
offset: options.resourceOffset,
|
|
14162
|
+
expectedFingerprint: diskDrifted ? `indexed:${diskFingerprint}` : diskFingerprint,
|
|
14163
|
+
});
|
|
14164
|
+
setBoundedCache(resourceReachabilityCache, cacheKey, { generation: indexGeneration, result: structuredClone(result) }, RESOURCE_REACHABILITY_CACHE_LIMIT);
|
|
14165
|
+
return finish([], { count: result.paths.length, resourceReachability: result });
|
|
14166
|
+
}
|
|
13342
14167
|
case "flow_analysis": {
|
|
13343
14168
|
// C8 deliberately does not persist a PDG. This is a bounded, local
|
|
13344
14169
|
// source read for one resolved TS/JS function-like symbol only.
|
|
@@ -14194,11 +15019,38 @@ export function createEngine() {
|
|
|
14194
15019
|
return result;
|
|
14195
15020
|
},
|
|
14196
15021
|
async close() {
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
for (const
|
|
15022
|
+
if (closed)
|
|
15023
|
+
return;
|
|
15024
|
+
closed = true;
|
|
15025
|
+
const releasable = new Set();
|
|
15026
|
+
for (const repo of ownedRepositories) {
|
|
15027
|
+
const owners = repositoryEngineOwners.get(repo);
|
|
15028
|
+
owners?.delete(owner);
|
|
15029
|
+
if (!owners?.size) {
|
|
15030
|
+
repositoryEngineOwners.delete(repo);
|
|
15031
|
+
releasable.add(repo);
|
|
15032
|
+
}
|
|
15033
|
+
}
|
|
15034
|
+
// When the final tracked engine closes, also reclaim legacy/federated
|
|
15035
|
+
// resources that predate ownership registration.
|
|
15036
|
+
if (repositoryEngineOwners.size === 0) {
|
|
15037
|
+
for (const repo of watchers.keys())
|
|
15038
|
+
releasable.add(repo);
|
|
15039
|
+
for (const repo of watchQueues.keys())
|
|
15040
|
+
releasable.add(repo);
|
|
15041
|
+
for (const repo of dbInstances.keys())
|
|
15042
|
+
releasable.add(repo);
|
|
15043
|
+
}
|
|
15044
|
+
for (const repo of releasable) {
|
|
15045
|
+
const watcher = watchers.get(repo);
|
|
15046
|
+
if (watcher)
|
|
15047
|
+
await watcher.close();
|
|
15048
|
+
watchers.delete(repo);
|
|
15049
|
+
}
|
|
15050
|
+
for (const repo of releasable) {
|
|
15051
|
+
const queue = watchQueues.get(repo);
|
|
15052
|
+
if (!queue)
|
|
15053
|
+
continue;
|
|
14202
15054
|
queue.closed = true;
|
|
14203
15055
|
if (queue.timer) {
|
|
14204
15056
|
clearTimeout(queue.timer);
|
|
@@ -14209,13 +15061,15 @@ export function createEngine() {
|
|
|
14209
15061
|
await queue.drain();
|
|
14210
15062
|
if (queue.pending.size > 0)
|
|
14211
15063
|
await queue.drain();
|
|
15064
|
+
watchQueues.delete(repo);
|
|
14212
15065
|
}
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
db
|
|
15066
|
+
for (const repo of releasable) {
|
|
15067
|
+
const db = dbInstances.get(repo);
|
|
15068
|
+
if (db)
|
|
15069
|
+
db.close();
|
|
15070
|
+
dbInstances.delete(repo);
|
|
15071
|
+
initPromises.delete(repo);
|
|
14216
15072
|
}
|
|
14217
|
-
dbInstances.clear();
|
|
14218
|
-
initPromises.clear();
|
|
14219
15073
|
freshnessProbes.clear();
|
|
14220
15074
|
mapCache.clear();
|
|
14221
15075
|
minimalMapCache.clear();
|
|
@@ -14230,9 +15084,11 @@ export function createEngine() {
|
|
|
14230
15084
|
annIndexCache.clear();
|
|
14231
15085
|
searchMetadataCache.clear();
|
|
14232
15086
|
searchCommunityLookupCache.clear();
|
|
15087
|
+
resourceCorpusCache.clear();
|
|
15088
|
+
resourceReachabilityCache.clear();
|
|
14233
15089
|
packageMapCache.clear();
|
|
14234
15090
|
reExportCache.clear();
|
|
14235
15091
|
},
|
|
14236
15092
|
};
|
|
14237
|
-
return withStaleness(engine);
|
|
15093
|
+
return withStaleness(engine, claimRepository);
|
|
14238
15094
|
}
|