knodin 0.12.2 → 0.13.0
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 +16 -1
- package/dist/bin/cli.js +173 -14
- package/dist/src/agent-events.js +25 -7
- package/dist/src/authenticated-cursor.js +81 -0
- package/dist/src/class-consumer-contract.js +18 -0
- package/dist/src/class-consumer-cursor.js +91 -0
- package/dist/src/class-consumer-delivery.js +22 -0
- package/dist/src/class-consumer-page.js +148 -0
- package/dist/src/cli-model.js +9 -3
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/apex-class-uses.js +430 -0
- package/dist/src/engine/apex-entry-points.js +98 -0
- package/dist/src/engine/apex-receiver.js +301 -0
- package/dist/src/engine/embedding-reuse.js +57 -0
- package/dist/src/engine/embeddings.js +22 -0
- package/dist/src/engine/index-coverage.js +215 -0
- package/dist/src/engine/index.js +2495 -252
- package/dist/src/engine/salesforce-components.js +460 -0
- package/dist/src/engine/seal.js +3 -0
- package/dist/src/engine/sqlite.js +44 -0
- package/dist/src/evidence-bundle.js +283 -0
- package/dist/src/evidence-graph.js +163 -0
- package/dist/src/failure-diagnosis.js +80 -5
- package/dist/src/file-dependency.js +35 -0
- package/dist/src/graph-query-health.js +47 -1
- package/dist/src/implementation-search.js +69 -0
- package/dist/src/index-coverage-read.js +33 -0
- package/dist/src/investigation.js +195 -0
- package/dist/src/mcp-reliability.js +4 -0
- package/dist/src/mcp-worker-supervisor.js +122 -6
- package/dist/src/progressive-evidence.js +4 -4
- package/dist/src/response-budget.js +129 -3
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/publisher.js +41 -1
- package/dist/src/tools/knodin-tools.js +224 -41
- package/docs/CLI.md +92 -0
- package/docs/MCP.md +67 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +62 -0
- package/docs/SALESFORCE-BINDINGS.md +121 -0
- package/docs/SALESFORCE-DEAD-CODE.md +45 -0
- package/docs/SCOPED-INDEXING.md +76 -0
- package/docs/apex-receiver-resolution.md +41 -0
- package/docs/releases/0.13.0.md +62 -0
- package/docs/structural-only-indexing.md +20 -0
- package/package.json +11 -5
package/dist/src/engine/index.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* This file implements the Phase 1: SQLite + Tree-Sitter Core engine.
|
|
12
12
|
*/
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
14
|
import child_process from "node:child_process";
|
|
14
15
|
import crypto from "node:crypto";
|
|
15
16
|
import fs from "node:fs";
|
|
@@ -18,21 +19,31 @@ import path from "node:path";
|
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
import chokidar from "chokidar";
|
|
20
21
|
import Parser from "web-tree-sitter";
|
|
22
|
+
import { ClassConsumerQueryError, } from "../class-consumer-contract.js";
|
|
23
|
+
import { readClassConsumerCursor } from "../class-consumer-cursor.js";
|
|
24
|
+
import { finalizeClassConsumerPage } from "../class-consumer-page.js";
|
|
21
25
|
import { compareBytes } from "../compare.js";
|
|
26
|
+
import { isFileRelationship } from "../file-dependency.js";
|
|
22
27
|
import { collectFileMetrics } from "../file-metrics.js";
|
|
23
28
|
import { readIndexActivity } from "../index-activity.js";
|
|
24
29
|
import { runReadOnlyLspQuery } from "../lsp-readonly.js";
|
|
30
|
+
import { evidenceHandleSecret } from "../progressive-evidence.js";
|
|
25
31
|
import { acquireRepairLease } from "../repair-lease.js";
|
|
26
32
|
import { analyzeResourceReachability, resourceFingerprint, } from "../resource-reachability.js";
|
|
27
33
|
import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snapshot.js";
|
|
28
34
|
import { acquireLifecycleCoordination } from "../update-coordination.js";
|
|
29
35
|
import { KNODIN_VERSION } from "../version.js";
|
|
30
36
|
import * as ann from "./ann-hnsw.js";
|
|
37
|
+
import { createApexConsumerCatalog, extractApexClassUses, resolveApexClassUses, } from "./apex-class-uses.js";
|
|
38
|
+
import { apexOrgEntryPoints } from "./apex-entry-points.js";
|
|
39
|
+
import { apexCall, apexDeclaration, apexReceiverCapability, apexReferenceReadGuard, parseApexDeclaration, persistApexReferenceEndpoints, } from "./apex-receiver.js";
|
|
31
40
|
import { allocateCandidate, assertCandidate, discardCandidateFiles, listCandidates, promoteCandidateFile, recoverInterruptedPromotion, sweepAbandonedCandidates, } from "./candidate-database.js";
|
|
32
|
-
import {
|
|
41
|
+
import { EmbeddingReuseCache, embeddingReuseKey } from "./embedding-reuse.js";
|
|
42
|
+
import { computeSimilarity, generateEmbedding, generateEmbeddings, getEmbeddingCacheIdentity, } from "./embeddings.js";
|
|
33
43
|
import { walkRepoFiles } from "./file-walker.js";
|
|
34
44
|
import { clearGitHistorySignalCache, collectGitHistorySignals, } from "./git-history.js";
|
|
35
45
|
import { isLinkedWorktree } from "./git-layout.js";
|
|
46
|
+
import { canonicalCoveragePath, coverageContains, coverageGitPathspecBatches, coverageMayContain, INDEX_COVERAGE_COMPLETE_KEY, INDEX_COVERAGE_KEY, INDEX_COVERAGE_PENDING_KEY, indexCoverageSummary, mergeIndexCoverage, readIndexCoverage, requireIndexCoverage, } from "./index-coverage.js";
|
|
36
47
|
// Runtime import, but not a cycle: parse-pool imports only TYPES from here,
|
|
37
48
|
// which erase at compile time. parse-worker's runtime import of this module
|
|
38
49
|
// resolves inside the worker thread, never in this one.
|
|
@@ -40,6 +51,7 @@ import { createParsePool, parseWorkersEnabled } from "./parse-pool.js";
|
|
|
40
51
|
import { beginPerfPhase, measurePerfPhase, measurePerfPhaseSync } from "./perf.js";
|
|
41
52
|
import { isIndexablePath, makeWatchIgnorePredicate } from "./prune.js";
|
|
42
53
|
import { reflinkCopyFile } from "./reflink-copy.js";
|
|
54
|
+
import { componentBindings, componentCatalog, visualforceInlineScripts, } from "./salesforce-components.js";
|
|
43
55
|
import { readSarifLog, SARIF_DEFAULT_LIMITS, } from "./sarif-import.js";
|
|
44
56
|
import { readScipIndex, SCIP_DEFAULT_LIMITS, } from "./scip-import.js";
|
|
45
57
|
import { isSealedDatabase, listSealedFiles, readSealedSource } from "./seal.js";
|
|
@@ -511,7 +523,7 @@ function referencesForFiles(db, filePaths) {
|
|
|
511
523
|
}
|
|
512
524
|
return [...rows.values()];
|
|
513
525
|
}
|
|
514
|
-
function persistSymbolIdentities(db, repoPath, filePaths) {
|
|
526
|
+
function persistSymbolIdentities(db, repoPath, filePaths, deferApex = false) {
|
|
515
527
|
const scope = scopedFilePaths(filePaths);
|
|
516
528
|
const rows = symbolsForFiles(db, scope);
|
|
517
529
|
const update = db.query("UPDATE symbols SET identity = ? WHERE id = ?");
|
|
@@ -579,7 +591,7 @@ function persistSymbolIdentities(db, repoPath, filePaths) {
|
|
|
579
591
|
update.run(identity, row.id);
|
|
580
592
|
}
|
|
581
593
|
update.finalize();
|
|
582
|
-
persistReferenceEndpoints(db, scope);
|
|
594
|
+
persistReferenceEndpoints(db, scope, deferApex);
|
|
583
595
|
db.run("RELEASE persist_symbol_identities;");
|
|
584
596
|
}
|
|
585
597
|
catch (error) {
|
|
@@ -599,15 +611,26 @@ async function persistSymbolIdentitiesCancellable(db, repoPath, filePaths, signa
|
|
|
599
611
|
: (scopedFilePaths(filePaths) ?? []);
|
|
600
612
|
for (const file of scope) {
|
|
601
613
|
throwIfAborted(signal);
|
|
602
|
-
persistSymbolIdentities(db, repoPath, [file]);
|
|
614
|
+
persistSymbolIdentities(db, repoPath, [file], true);
|
|
603
615
|
await yieldToIndexEventLoop();
|
|
604
616
|
}
|
|
617
|
+
if (scope.some((file) => /\.(?:cls|trigger)$/i.test(file))) {
|
|
618
|
+
persistApexReferenceEndpoints(db);
|
|
619
|
+
persistApexClassConsumerEndpoints(db);
|
|
620
|
+
}
|
|
605
621
|
}
|
|
606
622
|
/** Bind name/file reference rows to exact definitions after identities exist. */
|
|
607
|
-
function persistReferenceEndpoints(db, filePaths) {
|
|
623
|
+
function persistReferenceEndpoints(db, filePaths, deferApex = false) {
|
|
624
|
+
const resolveApex = !deferApex &&
|
|
625
|
+
(filePaths === undefined || filePaths.some((file) => /\.(?:cls|trigger)$/i.test(file)));
|
|
608
626
|
const refs = referencesForFiles(db, filePaths);
|
|
609
|
-
if (refs.length === 0)
|
|
627
|
+
if (refs.length === 0) {
|
|
628
|
+
if (resolveApex) {
|
|
629
|
+
persistApexReferenceEndpoints(db);
|
|
630
|
+
persistApexClassConsumerEndpoints(db);
|
|
631
|
+
}
|
|
610
632
|
return;
|
|
633
|
+
}
|
|
611
634
|
const definitionFiles = filePaths === undefined
|
|
612
635
|
? undefined
|
|
613
636
|
: [
|
|
@@ -670,7 +693,7 @@ function persistReferenceEndpoints(db, filePaths) {
|
|
|
670
693
|
const caller = choose(ref.callerSymbol, ref.callerFile, ref.line);
|
|
671
694
|
// Never invent an endpoint for an ambiguous name/file pair. Parsers can
|
|
672
695
|
// later provide a qualified identity; until then ambiguity remains explicit.
|
|
673
|
-
const callee = ref.calleeFile
|
|
696
|
+
const callee = ref.calleeFile && !ref.apexCall
|
|
674
697
|
? chooseScopedCallee(ref.calleeSymbol, ref.calleeFile, ref.line)
|
|
675
698
|
: undefined;
|
|
676
699
|
const callerIdentity = caller?.identity ?? null;
|
|
@@ -680,6 +703,835 @@ function persistReferenceEndpoints(db, filePaths) {
|
|
|
680
703
|
}
|
|
681
704
|
}
|
|
682
705
|
update.finalize();
|
|
706
|
+
if (resolveApex) {
|
|
707
|
+
persistApexReferenceEndpoints(db);
|
|
708
|
+
persistApexClassConsumerEndpoints(db);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const APEX_CONSUMER_BINDING_VERSION = "2-interface-types";
|
|
712
|
+
/** Classes retain receiver provenance; interfaces use their independent parser span proof. */
|
|
713
|
+
function apexConsumerSymbolOwner(db, symbol, cache) {
|
|
714
|
+
if (symbol.kind === "class" &&
|
|
715
|
+
!cache
|
|
716
|
+
.get(symbol.filePath)
|
|
717
|
+
?.declarations.some((declaration) => declaration.kind === "class" &&
|
|
718
|
+
declaration.name === symbol.name &&
|
|
719
|
+
declaration.declarationRange?.start.line === symbol.startLine &&
|
|
720
|
+
declaration.declarationRange.start.column - 1 === symbol.startCol))
|
|
721
|
+
return parseApexDeclaration(symbol.apexDeclaration)?.owner ?? null;
|
|
722
|
+
if (symbol.kind !== "interface" && symbol.kind !== "class")
|
|
723
|
+
return null;
|
|
724
|
+
if (!cache.has(symbol.filePath)) {
|
|
725
|
+
const row = db
|
|
726
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath WHERE f.filePath=?")
|
|
727
|
+
.get(symbol.filePath);
|
|
728
|
+
cache.set(symbol.filePath, row ? readApexConsumerFacts(row) : null);
|
|
729
|
+
}
|
|
730
|
+
const facts = cache.get(symbol.filePath);
|
|
731
|
+
if (facts?.parseState !== "complete")
|
|
732
|
+
return null;
|
|
733
|
+
const matches = facts.declarations.filter((declaration) => {
|
|
734
|
+
const range = declaration.declarationRange;
|
|
735
|
+
return (declaration.kind === symbol.kind &&
|
|
736
|
+
declaration.name === symbol.name &&
|
|
737
|
+
range &&
|
|
738
|
+
range.start.line === symbol.startLine &&
|
|
739
|
+
range.start.column - 1 === symbol.startCol &&
|
|
740
|
+
range.end.line === symbol.endLine &&
|
|
741
|
+
range.end.column - 1 === symbol.endCol);
|
|
742
|
+
});
|
|
743
|
+
return matches.length === 1 ? matches[0].ownerPath : null;
|
|
744
|
+
}
|
|
745
|
+
const apexConsumerKinds = new Set([
|
|
746
|
+
"static_field_read",
|
|
747
|
+
"constructor",
|
|
748
|
+
"variable_type",
|
|
749
|
+
"cast",
|
|
750
|
+
"class_literal",
|
|
751
|
+
"parameter_type",
|
|
752
|
+
"generic_parameter_type",
|
|
753
|
+
"loop_variable_type",
|
|
754
|
+
"generic_type_argument",
|
|
755
|
+
]);
|
|
756
|
+
function validConsumerPosition(value) {
|
|
757
|
+
if (!value || typeof value !== "object")
|
|
758
|
+
return false;
|
|
759
|
+
const p = value;
|
|
760
|
+
return ["line", "column", "utf16Offset", "byte"].every((key) => Number.isSafeInteger(p[key]) &&
|
|
761
|
+
p[key] >= (key === "line" || key === "column" ? 1 : 0));
|
|
762
|
+
}
|
|
763
|
+
function validConsumerSite(value) {
|
|
764
|
+
if (!value || typeof value !== "object")
|
|
765
|
+
return false;
|
|
766
|
+
const site = value;
|
|
767
|
+
return (site.version === 1 &&
|
|
768
|
+
apexConsumerKinds.has(site.kind) &&
|
|
769
|
+
typeof site.qualifiedSpelling === "string" &&
|
|
770
|
+
typeof site.typePath === "string" &&
|
|
771
|
+
/^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/.test(site.typePath) &&
|
|
772
|
+
(site.memberName === null || typeof site.memberName === "string") &&
|
|
773
|
+
(site.lexicalOwner === null || typeof site.lexicalOwner === "string") &&
|
|
774
|
+
[null, "value-shadow", "malformed-source"].includes(site.blockedReason) &&
|
|
775
|
+
validConsumerPosition(site.start) &&
|
|
776
|
+
validConsumerPosition(site.end) &&
|
|
777
|
+
site.end.byte > site.start.byte &&
|
|
778
|
+
site.end.utf16Offset > site.start.utf16Offset);
|
|
779
|
+
}
|
|
780
|
+
function readApexConsumerFacts(row) {
|
|
781
|
+
if (row.version !== 1 ||
|
|
782
|
+
row.facts.length > 32 * 1024 * 1024 ||
|
|
783
|
+
crypto.createHash("sha256").update(row.facts).digest("hex") !== row.factsHash)
|
|
784
|
+
return null;
|
|
785
|
+
try {
|
|
786
|
+
const facts = JSON.parse(row.facts);
|
|
787
|
+
if (facts.heritageComplete !== undefined && typeof facts.heritageComplete !== "boolean")
|
|
788
|
+
return null;
|
|
789
|
+
if (facts.heritage !== undefined &&
|
|
790
|
+
(!Array.isArray(facts.heritage) ||
|
|
791
|
+
!facts.heritage.every((entry) => ["extends", "implements"].includes(entry.kind) &&
|
|
792
|
+
typeof entry.ownerPath === "string" &&
|
|
793
|
+
typeof entry.typePath === "string" &&
|
|
794
|
+
/^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/.test(entry.typePath) &&
|
|
795
|
+
validConsumerPosition(entry.start) &&
|
|
796
|
+
validConsumerPosition(entry.end) &&
|
|
797
|
+
entry.start.byte < entry.end.byte)))
|
|
798
|
+
return null;
|
|
799
|
+
if ("omissionReason" in facts &&
|
|
800
|
+
(!["source-size-limit", "parser-unavailable", "parser-failure"].includes(String(facts.omissionReason)) ||
|
|
801
|
+
facts.parseState !== "partial" ||
|
|
802
|
+
facts.isTest !== null ||
|
|
803
|
+
!Array.isArray(facts.sites) ||
|
|
804
|
+
facts.sites.length !== 0 ||
|
|
805
|
+
!Array.isArray(facts.declarations) ||
|
|
806
|
+
facts.declarations.length !== 0 ||
|
|
807
|
+
!Array.isArray(facts.unsupportedUses) ||
|
|
808
|
+
facts.unsupportedUses.length !== 0))
|
|
809
|
+
return null;
|
|
810
|
+
if (facts.version !== 1 ||
|
|
811
|
+
facts.file !== row.filePath ||
|
|
812
|
+
facts.sourceHash !== row.sourceHash ||
|
|
813
|
+
!/^[a-f0-9]{64}$/.test(facts.sourceHash) ||
|
|
814
|
+
facts.parseState !== row.parseState ||
|
|
815
|
+
!["complete", "partial"].includes(facts.parseState) ||
|
|
816
|
+
facts.coverage !== "supported-syntax-only" ||
|
|
817
|
+
!(facts.isTest === null || typeof facts.isTest === "boolean") ||
|
|
818
|
+
(facts.isTest === null ? null : facts.isTest ? 1 : 0) !== row.isTest ||
|
|
819
|
+
!Array.isArray(facts.sites) ||
|
|
820
|
+
!facts.sites.every(validConsumerSite) ||
|
|
821
|
+
!Array.isArray(facts.declarations) ||
|
|
822
|
+
!Array.isArray(facts.unsupportedUses))
|
|
823
|
+
return null;
|
|
824
|
+
if (!facts.declarations.every((decl) => decl.version === 1 &&
|
|
825
|
+
["class", "interface", "enum", "field", "property"].includes(decl.kind) &&
|
|
826
|
+
typeof decl.ownerPath === "string" &&
|
|
827
|
+
typeof decl.name === "string" &&
|
|
828
|
+
typeof decl.static === "boolean" &&
|
|
829
|
+
(decl.superclass === null || typeof decl.superclass === "string") &&
|
|
830
|
+
Array.isArray(decl.annotations) &&
|
|
831
|
+
decl.annotations.every((a) => typeof a === "string") &&
|
|
832
|
+
validConsumerPosition(decl.start) &&
|
|
833
|
+
validConsumerPosition(decl.end) &&
|
|
834
|
+
(decl.declarationRange === undefined ||
|
|
835
|
+
(validConsumerPosition(decl.declarationRange.start) &&
|
|
836
|
+
validConsumerPosition(decl.declarationRange.end) &&
|
|
837
|
+
decl.declarationRange.start.byte <= decl.start.byte &&
|
|
838
|
+
decl.declarationRange.end.byte >= decl.end.byte))))
|
|
839
|
+
return null;
|
|
840
|
+
return facts;
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/** Rebind retained typed facts after identities exist, without reading repository source. */
|
|
847
|
+
function apexConsumerMaterializationDigest(db) {
|
|
848
|
+
const hash = crypto.createHash("sha256");
|
|
849
|
+
let after;
|
|
850
|
+
for (;;) {
|
|
851
|
+
const rows = db
|
|
852
|
+
.query(`SELECT filePath,startByte,endByte,kind,raw,status,targetFile,targetOwner,targetIdentity,ownerIdentity,isTest FROM apex_class_uses ${after ? "WHERE (filePath COLLATE BINARY,startByte,endByte,kind COLLATE BINARY)>(? COLLATE BINARY,?,?,? COLLATE BINARY)" : ""} ORDER BY filePath COLLATE BINARY,startByte,endByte,kind COLLATE BINARY LIMIT 500`)
|
|
853
|
+
.all(...(after ? [after.filePath, after.startByte, after.endByte, after.kind] : []));
|
|
854
|
+
for (const row of rows)
|
|
855
|
+
hash.update(JSON.stringify(row)).update("\n");
|
|
856
|
+
if (rows.length < 500)
|
|
857
|
+
break;
|
|
858
|
+
after = rows[rows.length - 1];
|
|
859
|
+
}
|
|
860
|
+
return hash.digest("hex");
|
|
861
|
+
}
|
|
862
|
+
function apexConsumerMaterializationValid(db) {
|
|
863
|
+
if (db
|
|
864
|
+
.query("SELECT COUNT(*) count FROM sqlite_master WHERE type='table' AND name IN ('apex_class_uses','apex_consumer_files')")
|
|
865
|
+
.get()?.count !== 2)
|
|
866
|
+
return false;
|
|
867
|
+
const stored = db
|
|
868
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath")
|
|
869
|
+
.all();
|
|
870
|
+
const facts = stored.map(readApexConsumerFacts);
|
|
871
|
+
if (facts.includes(null))
|
|
872
|
+
return false;
|
|
873
|
+
const expected = facts.reduce((sum, fact) => sum + (fact?.sites.length ?? 0), 0);
|
|
874
|
+
if ((db.query("SELECT COUNT(*) count FROM apex_class_uses").get()?.count ??
|
|
875
|
+
0) !== expected)
|
|
876
|
+
return false;
|
|
877
|
+
return Boolean(getMeta(db, "apexConsumerBindingVersion") === APEX_CONSUMER_BINDING_VERSION &&
|
|
878
|
+
getMeta(db, "apexConsumerFactsRevision") &&
|
|
879
|
+
getMeta(db, "apexConsumerFactsRevision") === getMeta(db, "apexConsumerBoundRevision") &&
|
|
880
|
+
getMeta(db, "apexConsumerMaterializationDigest") === apexConsumerMaterializationDigest(db));
|
|
881
|
+
}
|
|
882
|
+
function persistApexClassConsumerEndpoints(db) {
|
|
883
|
+
const stored = db
|
|
884
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath")
|
|
885
|
+
.all();
|
|
886
|
+
const facts = stored
|
|
887
|
+
.map(readApexConsumerFacts)
|
|
888
|
+
.filter((fact) => fact !== null);
|
|
889
|
+
const catalog = createApexConsumerCatalog(facts);
|
|
890
|
+
const symbols = db
|
|
891
|
+
.query("SELECT * FROM symbols WHERE kind IN ('class','interface')")
|
|
892
|
+
.all();
|
|
893
|
+
const factsByFile = new Map(facts.map((file) => [file.file, file]));
|
|
894
|
+
const identities = new Map();
|
|
895
|
+
for (const symbol of symbols) {
|
|
896
|
+
const owner = apexConsumerSymbolOwner(db, symbol, factsByFile);
|
|
897
|
+
if (!owner || !symbol.identity)
|
|
898
|
+
continue;
|
|
899
|
+
const key = `${symbol.filePath}\0${owner.toLowerCase()}`;
|
|
900
|
+
const entries = identities.get(key) ?? [];
|
|
901
|
+
entries.push(symbol.identity);
|
|
902
|
+
identities.set(key, entries);
|
|
903
|
+
}
|
|
904
|
+
const identity = (file, owner) => {
|
|
905
|
+
const candidates = owner ? identities.get(`${file}\0${owner.toLowerCase()}`) : undefined;
|
|
906
|
+
return candidates?.length === 1 ? candidates[0] : null;
|
|
907
|
+
};
|
|
908
|
+
db.run("SAVEPOINT apex_consumer_rebind");
|
|
909
|
+
try {
|
|
910
|
+
db.run("DELETE FROM apex_class_uses");
|
|
911
|
+
const insert = db.prepare("INSERT INTO apex_class_uses(filePath,startByte,endByte,kind,raw,status,targetFile,targetOwner,targetIdentity,ownerIdentity,isTest) VALUES (?,?,?,?,?,?,?,?,?,?,?)");
|
|
912
|
+
try {
|
|
913
|
+
for (const file of facts) {
|
|
914
|
+
for (const resolved of resolveApexClassUses(catalog, file)) {
|
|
915
|
+
const targetIdentity = resolved.target
|
|
916
|
+
? identity(resolved.target.file, resolved.target.declaration.ownerPath)
|
|
917
|
+
: null;
|
|
918
|
+
const status = stored.length !== facts.length || (resolved.status === "resolved" && !targetIdentity)
|
|
919
|
+
? "blocked"
|
|
920
|
+
: resolved.status;
|
|
921
|
+
insert.run(file.file, resolved.site.start.byte, resolved.site.end.byte, resolved.site.kind, JSON.stringify(resolved.site), status, resolved.target?.file ?? null, resolved.target?.declaration.ownerPath ?? null, status === "resolved" ? targetIdentity : null, identity(file.file, resolved.site.lexicalOwner), file.isTest === null ? null : file.isTest ? 1 : 0);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
finally {
|
|
926
|
+
insert.finalize();
|
|
927
|
+
}
|
|
928
|
+
setMeta(db, "apexConsumerBoundRevision", getMeta(db, "apexConsumerFactsRevision") ?? "");
|
|
929
|
+
setMeta(db, "apexConsumerBindingVersion", APEX_CONSUMER_BINDING_VERSION);
|
|
930
|
+
setMeta(db, "apexConsumerMaterializationDigest", apexConsumerMaterializationDigest(db));
|
|
931
|
+
db.run("RELEASE apex_consumer_rebind");
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
db.run("ROLLBACK TO apex_consumer_rebind");
|
|
935
|
+
db.run("RELEASE apex_consumer_rebind");
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function missingApexConsumerDescriptorFiles(db) {
|
|
940
|
+
const files = db
|
|
941
|
+
.query("SELECT filePath FROM index_state WHERE lower(filePath) LIKE '%.cls' OR lower(filePath) LIKE '%.trigger'")
|
|
942
|
+
.all()
|
|
943
|
+
.map((row) => row.filePath);
|
|
944
|
+
if (!files.length)
|
|
945
|
+
return [];
|
|
946
|
+
const hasTable = db
|
|
947
|
+
.query("SELECT COUNT(*) count FROM sqlite_master WHERE type='table' AND name='apex_consumer_files'")
|
|
948
|
+
.get()?.count === 1;
|
|
949
|
+
if (!hasTable)
|
|
950
|
+
return files;
|
|
951
|
+
const valid = new Set(db
|
|
952
|
+
.query("SELECT * FROM apex_consumer_files")
|
|
953
|
+
.all()
|
|
954
|
+
.filter((row) => {
|
|
955
|
+
const facts = readApexConsumerFacts(row);
|
|
956
|
+
if (!facts)
|
|
957
|
+
return false;
|
|
958
|
+
if (facts.parseState !== "complete")
|
|
959
|
+
return true;
|
|
960
|
+
if (facts.heritage === undefined &&
|
|
961
|
+
facts.unsupportedUses.some((use) => use.kind === "inheritance"))
|
|
962
|
+
return false;
|
|
963
|
+
const interfaces = facts.declarations.filter((d) => d.kind === "interface");
|
|
964
|
+
if (!interfaces.length)
|
|
965
|
+
return true;
|
|
966
|
+
const symbols = db
|
|
967
|
+
.query("SELECT * FROM symbols WHERE filePath=? AND kind='interface' AND identity IS NOT NULL")
|
|
968
|
+
.all(row.filePath);
|
|
969
|
+
const cache = new Map([[row.filePath, facts]]);
|
|
970
|
+
return interfaces.every((declaration) => declaration.declarationRange &&
|
|
971
|
+
symbols.some((symbol) => apexConsumerSymbolOwner(db, symbol, cache) === declaration.ownerPath));
|
|
972
|
+
})
|
|
973
|
+
.map((row) => row.filePath));
|
|
974
|
+
return files.filter((file) => !valid.has(file));
|
|
975
|
+
}
|
|
976
|
+
const apexConsumerValidationMemo = new Map();
|
|
977
|
+
const apexConsumerFastHits = new WeakMap();
|
|
978
|
+
const apexConsumerValidationMemoStats = new WeakMap();
|
|
979
|
+
const APEX_CONSUMER_MEMO_MAX_ENTRIES = 4;
|
|
980
|
+
const APEX_CONSUMER_MEMO_MAX_BYTES = 8 * 1024 * 1024;
|
|
981
|
+
/** Diagnostic counters only; never expose retained source or descriptors. */
|
|
982
|
+
export function getApexConsumerValidationMemoStats(db) {
|
|
983
|
+
return {
|
|
984
|
+
...(apexConsumerValidationMemoStats.get(db) ?? { hits: 0, misses: 0 }),
|
|
985
|
+
retainedBytes: apexConsumerValidationMemo.get(db)?.retainedBytes ?? 0,
|
|
986
|
+
fastHits: apexConsumerFastHits.get(db) ?? 0,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Normal SQLite commit/snapshot semantics, not protection against malicious counter or file manipulation. */
|
|
990
|
+
function apexConsumerSnapshotStamp(db, transaction) {
|
|
991
|
+
const identity = db.readSnapshotIdentity();
|
|
992
|
+
if (identity.replaced) {
|
|
993
|
+
apexConsumerValidationMemo.delete(db);
|
|
994
|
+
throw new Error("Graph database file was replaced; reopen the engine before querying.");
|
|
995
|
+
}
|
|
996
|
+
if (identity.transaction !== transaction || identity.fileStamp === null)
|
|
997
|
+
return null;
|
|
998
|
+
try {
|
|
999
|
+
// This read lazily opens TEMP on a fresh connection. Normalize it before
|
|
1000
|
+
// sampling attachments so our own inspection does not invalidate the proof.
|
|
1001
|
+
const tempSchemaVersion = db
|
|
1002
|
+
.query("PRAGMA temp.schema_version")
|
|
1003
|
+
.get()?.schema_version;
|
|
1004
|
+
const attachments = db.query("PRAGMA database_list").all();
|
|
1005
|
+
if (attachments.some((row) => row.name !== "main" && row.name !== "temp"))
|
|
1006
|
+
return null;
|
|
1007
|
+
if (db.query("PRAGMA read_uncommitted").get()
|
|
1008
|
+
?.read_uncommitted !== 0)
|
|
1009
|
+
return null;
|
|
1010
|
+
const values = [
|
|
1011
|
+
db.query("PRAGMA main.data_version").get()?.data_version,
|
|
1012
|
+
db.query("SELECT total_changes() value").get()?.value,
|
|
1013
|
+
db.query("PRAGMA main.schema_version").get()?.schema_version,
|
|
1014
|
+
tempSchemaVersion,
|
|
1015
|
+
];
|
|
1016
|
+
if (!values.every((value) => Number.isSafeInteger(value) && value >= 0))
|
|
1017
|
+
return null;
|
|
1018
|
+
const schema = db
|
|
1019
|
+
.query("SELECT 'main' origin,name,sql FROM main.sqlite_master UNION ALL SELECT 'temp' origin,name,sql FROM sqlite_temp_master ORDER BY origin COLLATE BINARY,name COLLATE BINARY")
|
|
1020
|
+
.all();
|
|
1021
|
+
return crypto
|
|
1022
|
+
.createHash("sha256")
|
|
1023
|
+
.update(JSON.stringify([identity.fileStamp, attachments, values, schema]))
|
|
1024
|
+
.digest("hex");
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function releaseApexConsumerRead(db, eligible, completed, proof) {
|
|
1031
|
+
try {
|
|
1032
|
+
db.run("RELEASE class_consumer_read");
|
|
1033
|
+
const afterStamp = eligible ? apexConsumerSnapshotStamp(db, false) : null;
|
|
1034
|
+
const cached = apexConsumerValidationMemo.get(db);
|
|
1035
|
+
if (cached) {
|
|
1036
|
+
if (completed && proof && afterStamp === proof.stamp && cached.key === proof.validationDigest)
|
|
1037
|
+
cached.fastProof = proof;
|
|
1038
|
+
else
|
|
1039
|
+
delete cached.fastProof;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
const cached = apexConsumerValidationMemo.get(db);
|
|
1044
|
+
if (cached)
|
|
1045
|
+
delete cached.fastProof;
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function validatedApexConsumerSummary(db, stored, key) {
|
|
1050
|
+
const stats = apexConsumerValidationMemoStats.get(db) ?? { hits: 0, misses: 0 };
|
|
1051
|
+
apexConsumerValidationMemoStats.set(db, stats);
|
|
1052
|
+
const cached = apexConsumerValidationMemo.get(db);
|
|
1053
|
+
if (cached?.key === key) {
|
|
1054
|
+
stats.hits++;
|
|
1055
|
+
apexConsumerValidationMemo.delete(db);
|
|
1056
|
+
apexConsumerValidationMemo.set(db, cached);
|
|
1057
|
+
return cached.summary;
|
|
1058
|
+
}
|
|
1059
|
+
stats.misses++;
|
|
1060
|
+
apexConsumerValidationMemo.delete(db);
|
|
1061
|
+
const valid = stored
|
|
1062
|
+
.map(readApexConsumerFacts)
|
|
1063
|
+
.filter((facts) => facts !== null);
|
|
1064
|
+
const summary = {
|
|
1065
|
+
validFiles: valid.length,
|
|
1066
|
+
invalidDescriptorFiles: stored.length - valid.length,
|
|
1067
|
+
partialFiles: valid.filter((facts) => facts.parseState !== "complete").length,
|
|
1068
|
+
expectedSites: valid.reduce((count, facts) => count + facts.sites.length, 0),
|
|
1069
|
+
unsupportedUses: valid.reduce((count, facts) => count + facts.unsupportedUses.length, 0),
|
|
1070
|
+
catalog: createApexConsumerCatalog(valid),
|
|
1071
|
+
};
|
|
1072
|
+
// Account for retained declaration strings/structure, not the much larger raw sites.
|
|
1073
|
+
// Byte and entry caps bound retention; this is not a precise V8 heap measurement.
|
|
1074
|
+
const retainedBytes = Buffer.byteLength(JSON.stringify([Array.from(summary.catalog.classes), Array.from(summary.catalog.members)])) + 256;
|
|
1075
|
+
if (summary.invalidDescriptorFiles === 0 && retainedBytes <= APEX_CONSUMER_MEMO_MAX_BYTES) {
|
|
1076
|
+
while (apexConsumerValidationMemo.size >= APEX_CONSUMER_MEMO_MAX_ENTRIES) {
|
|
1077
|
+
const oldest = apexConsumerValidationMemo.keys().next().value;
|
|
1078
|
+
if (!oldest)
|
|
1079
|
+
break;
|
|
1080
|
+
apexConsumerValidationMemo.delete(oldest);
|
|
1081
|
+
}
|
|
1082
|
+
apexConsumerValidationMemo.set(db, { key, summary, retainedBytes });
|
|
1083
|
+
}
|
|
1084
|
+
return summary;
|
|
1085
|
+
}
|
|
1086
|
+
/** @internal Snapshot reader; callers must establish repository freshness before entry. */
|
|
1087
|
+
export function readClassConsumerPage(db, repoPath, request) {
|
|
1088
|
+
if (!request.symbol ||
|
|
1089
|
+
!Number.isSafeInteger(request.limit) ||
|
|
1090
|
+
request.limit < 1 ||
|
|
1091
|
+
request.limit > 1000 ||
|
|
1092
|
+
!["all", "test", "production"].includes(request.testScope))
|
|
1093
|
+
throw new Error("Invalid class-consumer query bounds");
|
|
1094
|
+
const filterPath = request.path?.replace(/\/$/, "");
|
|
1095
|
+
if (filterPath &&
|
|
1096
|
+
(path.posix.normalize(filterPath) !== filterPath ||
|
|
1097
|
+
filterPath.startsWith("/") ||
|
|
1098
|
+
filterPath.split("/").includes("..") ||
|
|
1099
|
+
filterPath.includes("\\")))
|
|
1100
|
+
throw new Error("Class-consumer path must be canonical and repository-relative");
|
|
1101
|
+
const after = request.after;
|
|
1102
|
+
if (after &&
|
|
1103
|
+
(!Number.isSafeInteger(after.startByte) ||
|
|
1104
|
+
!Number.isSafeInteger(after.endByte) ||
|
|
1105
|
+
after.startByte < 0 ||
|
|
1106
|
+
after.endByte <= after.startByte ||
|
|
1107
|
+
!apexConsumerKinds.has(after.kind) ||
|
|
1108
|
+
path.posix.normalize(after.file) !== after.file ||
|
|
1109
|
+
after.file.startsWith("/") ||
|
|
1110
|
+
after.file.split("/").includes("..")))
|
|
1111
|
+
throw new Error("Invalid class-consumer page key");
|
|
1112
|
+
const beforeStamp = apexConsumerSnapshotStamp(db, false);
|
|
1113
|
+
if (!beforeStamp) {
|
|
1114
|
+
const cached = apexConsumerValidationMemo.get(db);
|
|
1115
|
+
if (cached)
|
|
1116
|
+
delete cached.fastProof;
|
|
1117
|
+
}
|
|
1118
|
+
let publishProof;
|
|
1119
|
+
let pinnedStamp = null;
|
|
1120
|
+
let completed = false;
|
|
1121
|
+
db.run("SAVEPOINT class_consumer_read");
|
|
1122
|
+
try {
|
|
1123
|
+
// SAVEPOINT alone is deferred: touch main to establish the read snapshot first.
|
|
1124
|
+
db.query("SELECT name FROM main.sqlite_master LIMIT 1").get();
|
|
1125
|
+
pinnedStamp = beforeStamp ? apexConsumerSnapshotStamp(db, true) : null;
|
|
1126
|
+
const memo = apexConsumerValidationMemo.get(db);
|
|
1127
|
+
const fastProof = pinnedStamp && pinnedStamp === beforeStamp && memo?.fastProof?.stamp === pinnedStamp
|
|
1128
|
+
? memo.fastProof
|
|
1129
|
+
: undefined;
|
|
1130
|
+
if (fastProof) {
|
|
1131
|
+
apexConsumerFastHits.set(db, (apexConsumerFastHits.get(db) ?? 0) + 1);
|
|
1132
|
+
const stats = apexConsumerValidationMemoStats.get(db);
|
|
1133
|
+
if (stats)
|
|
1134
|
+
stats.hits++;
|
|
1135
|
+
}
|
|
1136
|
+
const indexCoverage = indexCoverageSummary(coverageForDatabase(db));
|
|
1137
|
+
const freshness = buildFreshnessEnvelope(repoPath, db, new Date().toISOString());
|
|
1138
|
+
const tablesPresent = (db
|
|
1139
|
+
.query("SELECT COUNT(*) count FROM sqlite_master WHERE type='table' AND name IN ('apex_consumer_files','apex_class_uses')")
|
|
1140
|
+
.get()?.count ?? 0) === 2;
|
|
1141
|
+
const stored = tablesPresent && !fastProof
|
|
1142
|
+
? db
|
|
1143
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath")
|
|
1144
|
+
.all()
|
|
1145
|
+
: [];
|
|
1146
|
+
const missingDescriptorFiles = db
|
|
1147
|
+
.query(tablesPresent
|
|
1148
|
+
? "SELECT COUNT(*) count FROM index_state i LEFT JOIN apex_consumer_files f ON f.filePath=i.filePath WHERE (lower(i.filePath) LIKE '%.cls' OR lower(i.filePath) LIKE '%.trigger') AND f.filePath IS NULL"
|
|
1149
|
+
: "SELECT COUNT(*) count FROM index_state WHERE lower(filePath) LIKE '%.cls' OR lower(filePath) LIKE '%.trigger'")
|
|
1150
|
+
.get()?.count ?? 0;
|
|
1151
|
+
const allClassRows = db
|
|
1152
|
+
.query("SELECT * FROM symbols WHERE kind IN ('class','interface') AND identity IS NOT NULL")
|
|
1153
|
+
.all()
|
|
1154
|
+
.filter((row) => /\.(?:cls|trigger)$/i.test(row.filePath));
|
|
1155
|
+
const typeFacts = new Map();
|
|
1156
|
+
if (tablesPresent)
|
|
1157
|
+
for (const row of allClassRows)
|
|
1158
|
+
if (row.kind === "interface")
|
|
1159
|
+
apexConsumerSymbolOwner(db, row, typeFacts);
|
|
1160
|
+
const typeOwner = (row) => tablesPresent
|
|
1161
|
+
? apexConsumerSymbolOwner(db, row, typeFacts)
|
|
1162
|
+
: row.kind === "class"
|
|
1163
|
+
? (parseApexDeclaration(row.apexDeclaration)?.owner ?? null)
|
|
1164
|
+
: null;
|
|
1165
|
+
const classRows = allClassRows.filter((row) => {
|
|
1166
|
+
if (row.name.toLowerCase() !== request.symbol.split(".").at(-1)?.toLowerCase())
|
|
1167
|
+
return false;
|
|
1168
|
+
if (tablesPresent && !typeFacts.has(row.filePath)) {
|
|
1169
|
+
const descriptor = db
|
|
1170
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath WHERE f.filePath=?")
|
|
1171
|
+
.get(row.filePath);
|
|
1172
|
+
typeFacts.set(row.filePath, descriptor ? readApexConsumerFacts(descriptor) : null);
|
|
1173
|
+
}
|
|
1174
|
+
return (typeOwner(row)?.toLowerCase() === request.symbol.toLowerCase() ||
|
|
1175
|
+
row.name.toLowerCase() === request.symbol.toLowerCase());
|
|
1176
|
+
});
|
|
1177
|
+
const selector = request.selector ?? {};
|
|
1178
|
+
const candidates = classRows.filter((row) => (!selector.file || row.filePath === selector.file) &&
|
|
1179
|
+
(!selector.kind || row.kind === selector.kind) &&
|
|
1180
|
+
(!selector.identity || symbolIdentityMatches(row.identity ?? "", selector.identity)));
|
|
1181
|
+
if (candidates.length === 0 &&
|
|
1182
|
+
tablesPresent &&
|
|
1183
|
+
!selector.identity &&
|
|
1184
|
+
(!selector.kind || selector.kind === "interface")) {
|
|
1185
|
+
const descriptorRows = stored.length
|
|
1186
|
+
? stored
|
|
1187
|
+
: db
|
|
1188
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath")
|
|
1189
|
+
.all();
|
|
1190
|
+
const retained = descriptorRows
|
|
1191
|
+
.map(readApexConsumerFacts)
|
|
1192
|
+
.filter((f) => f !== null);
|
|
1193
|
+
const competitors = retained.flatMap((f) => f.declarations
|
|
1194
|
+
.filter((d) => ["class", "interface", "enum"].includes(d.kind) &&
|
|
1195
|
+
(d.name.toLowerCase() === request.symbol.toLowerCase() ||
|
|
1196
|
+
d.ownerPath.toLowerCase() === request.symbol.toLowerCase()))
|
|
1197
|
+
.map((d) => ({ file: f.file, state: f.parseState, declaration: d })));
|
|
1198
|
+
if (competitors.length === 1 &&
|
|
1199
|
+
competitors[0].state === "complete" &&
|
|
1200
|
+
competitors[0].declaration.kind === "interface" &&
|
|
1201
|
+
(!selector.file || selector.file === competitors[0].file))
|
|
1202
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_TARGET_CAPABILITY_MISSING", "An indexed interface declaration exists but its typed symbol capability is missing; repair the indexed scope.");
|
|
1203
|
+
}
|
|
1204
|
+
if (candidates.length !== 1) {
|
|
1205
|
+
if (candidates.length === 0)
|
|
1206
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_TARGET_NOT_FOUND", "No indexed Apex class matches the requested target and selectors.");
|
|
1207
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_TARGET_AMBIGUOUS", "Several indexed Apex classes match; select an exact identity or file.", candidates.slice(0, 100).map((row, index) => ({
|
|
1208
|
+
identity: row.identity,
|
|
1209
|
+
symbol: row.name,
|
|
1210
|
+
file: row.filePath,
|
|
1211
|
+
kind: row.kind,
|
|
1212
|
+
line: row.startLine,
|
|
1213
|
+
rank: index + 1,
|
|
1214
|
+
})));
|
|
1215
|
+
}
|
|
1216
|
+
const selected = candidates[0];
|
|
1217
|
+
const targetIdentity = selected.identity;
|
|
1218
|
+
const targetOwner = typeOwner(selected);
|
|
1219
|
+
if (!targetOwner && selected.kind === "interface")
|
|
1220
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_TARGET_CAPABILITY_MISSING", "The indexed interface lacks validated declaration-to-symbol provenance; repair the indexed scope.");
|
|
1221
|
+
if (!targetOwner)
|
|
1222
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_TARGET_NOT_FOUND", "No validated Apex type declaration matches the target.");
|
|
1223
|
+
const targetKind = selected.kind;
|
|
1224
|
+
const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
1225
|
+
const queryDigest = digest({
|
|
1226
|
+
version: 1,
|
|
1227
|
+
repository: path.resolve(repoPath),
|
|
1228
|
+
targetIdentity,
|
|
1229
|
+
targetKind,
|
|
1230
|
+
bindingVersion: APEX_CONSUMER_BINDING_VERSION,
|
|
1231
|
+
testScope: request.testScope,
|
|
1232
|
+
path: filterPath ?? null,
|
|
1233
|
+
});
|
|
1234
|
+
const factsRevision = getMeta(db, "apexConsumerFactsRevision");
|
|
1235
|
+
const boundRevision = getMeta(db, "apexConsumerBoundRevision");
|
|
1236
|
+
const bindingVersion = getMeta(db, "apexConsumerBindingVersion");
|
|
1237
|
+
const descriptorDigest = fastProof?.descriptorDigest ??
|
|
1238
|
+
digest(stored
|
|
1239
|
+
.map((row) => [
|
|
1240
|
+
row.filePath,
|
|
1241
|
+
row.version,
|
|
1242
|
+
row.sourceHash,
|
|
1243
|
+
row.factsHash,
|
|
1244
|
+
digest(row.facts),
|
|
1245
|
+
row.parseState,
|
|
1246
|
+
row.isTest,
|
|
1247
|
+
])
|
|
1248
|
+
.sort((a, b) => compareBytes(String(a[0]), String(b[0]))));
|
|
1249
|
+
const materializedSites = tablesPresent
|
|
1250
|
+
? (db.query("SELECT COUNT(*) count FROM apex_class_uses").get()
|
|
1251
|
+
?.count ?? 0)
|
|
1252
|
+
: 0;
|
|
1253
|
+
const materializedDigest = fastProof
|
|
1254
|
+
? fastProof.materializedDigest
|
|
1255
|
+
: tablesPresent
|
|
1256
|
+
? apexConsumerMaterializationDigest(db)
|
|
1257
|
+
: null;
|
|
1258
|
+
// Unknown/changed native snapshots retain the full actual-byte scan, never revision-only trust.
|
|
1259
|
+
const inventory = fastProof
|
|
1260
|
+
? []
|
|
1261
|
+
: db
|
|
1262
|
+
.query("SELECT filePath FROM index_state WHERE lower(filePath) LIKE '%.cls' OR lower(filePath) LIKE '%.trigger' ORDER BY filePath COLLATE BINARY")
|
|
1263
|
+
.all();
|
|
1264
|
+
const schema = fastProof
|
|
1265
|
+
? []
|
|
1266
|
+
: db
|
|
1267
|
+
.query("SELECT 'main' origin,name,sql FROM sqlite_master UNION ALL SELECT 'temp' origin,name,sql FROM sqlite_temp_master ORDER BY origin COLLATE BINARY,name COLLATE BINARY")
|
|
1268
|
+
.all();
|
|
1269
|
+
const validationDigest = fastProof?.validationDigest ??
|
|
1270
|
+
digest({
|
|
1271
|
+
version: 1,
|
|
1272
|
+
descriptorDigest,
|
|
1273
|
+
inventory,
|
|
1274
|
+
schema,
|
|
1275
|
+
tablesPresent,
|
|
1276
|
+
missingDescriptorFiles,
|
|
1277
|
+
materializedSites,
|
|
1278
|
+
materializedDigest,
|
|
1279
|
+
factsRevision,
|
|
1280
|
+
boundRevision,
|
|
1281
|
+
bindingVersion,
|
|
1282
|
+
expectedMaterialization: getMeta(db, "apexConsumerMaterializationDigest"),
|
|
1283
|
+
});
|
|
1284
|
+
const validation = fastProof && memo ? memo.summary : validatedApexConsumerSummary(db, stored, validationDigest);
|
|
1285
|
+
const { expectedSites, invalidDescriptorFiles, partialFiles, unsupportedUses, catalog } = validation;
|
|
1286
|
+
// Fresh symbol proof is intentionally outside both validation caches. A removed outer
|
|
1287
|
+
// interface must not turn retained nested-class rows into a certified zero result.
|
|
1288
|
+
const missingInterfaceBindings = new Set();
|
|
1289
|
+
const interfaceSymbolSpans = new Set(allClassRows
|
|
1290
|
+
.filter((row) => row.kind === "interface")
|
|
1291
|
+
.map((row) => JSON.stringify([
|
|
1292
|
+
row.filePath,
|
|
1293
|
+
row.name,
|
|
1294
|
+
row.startLine,
|
|
1295
|
+
row.startCol,
|
|
1296
|
+
row.endLine,
|
|
1297
|
+
row.endCol,
|
|
1298
|
+
])));
|
|
1299
|
+
for (const entries of catalog.classes.values())
|
|
1300
|
+
for (const entry of entries) {
|
|
1301
|
+
if (entry.declaration.kind !== "interface" || entry.parseState !== "complete")
|
|
1302
|
+
continue;
|
|
1303
|
+
const range = entry.declaration.declarationRange;
|
|
1304
|
+
if (!range ||
|
|
1305
|
+
!interfaceSymbolSpans.has(JSON.stringify([
|
|
1306
|
+
entry.file,
|
|
1307
|
+
entry.declaration.name,
|
|
1308
|
+
range.start.line,
|
|
1309
|
+
range.start.column - 1,
|
|
1310
|
+
range.end.line,
|
|
1311
|
+
range.end.column - 1,
|
|
1312
|
+
])))
|
|
1313
|
+
missingInterfaceBindings.add(entry.file);
|
|
1314
|
+
}
|
|
1315
|
+
const snapshotDigest = digest({
|
|
1316
|
+
version: 1,
|
|
1317
|
+
queryDigest,
|
|
1318
|
+
validationDigest,
|
|
1319
|
+
classBindings: allClassRows
|
|
1320
|
+
.map((row) => [
|
|
1321
|
+
row.filePath,
|
|
1322
|
+
row.name,
|
|
1323
|
+
row.kind,
|
|
1324
|
+
row.identity,
|
|
1325
|
+
row.apexDeclaration,
|
|
1326
|
+
row.startLine,
|
|
1327
|
+
row.startCol,
|
|
1328
|
+
row.endLine,
|
|
1329
|
+
row.endCol,
|
|
1330
|
+
])
|
|
1331
|
+
.sort((a, b) => compareBytes(JSON.stringify(a), JSON.stringify(b))),
|
|
1332
|
+
factsRevision,
|
|
1333
|
+
boundRevision,
|
|
1334
|
+
bindingVersion,
|
|
1335
|
+
descriptorDigest,
|
|
1336
|
+
materializedSites,
|
|
1337
|
+
materializedDigest,
|
|
1338
|
+
indexCoverage,
|
|
1339
|
+
head: freshness.currentHead,
|
|
1340
|
+
indexedHead: freshness.indexedHead,
|
|
1341
|
+
workingTree: freshness.workingTree,
|
|
1342
|
+
});
|
|
1343
|
+
if (request.expectedQueryDigest && request.expectedQueryDigest !== queryDigest)
|
|
1344
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_QUERY_MISMATCH", "The continuation does not describe this class-consumer query.");
|
|
1345
|
+
if (request.expectedSnapshotDigest && request.expectedSnapshotDigest !== snapshotDigest)
|
|
1346
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_STALE_CURSOR", "The indexed graph or coverage changed; restart class-consumer paging.");
|
|
1347
|
+
const unresolvedSites = tablesPresent
|
|
1348
|
+
? (db
|
|
1349
|
+
.query("SELECT COUNT(*) count FROM apex_class_uses WHERE status<>'resolved'")
|
|
1350
|
+
.get()?.count ?? 0)
|
|
1351
|
+
: 0;
|
|
1352
|
+
const bound = Boolean(tablesPresent &&
|
|
1353
|
+
bindingVersion === APEX_CONSUMER_BINDING_VERSION &&
|
|
1354
|
+
factsRevision &&
|
|
1355
|
+
boundRevision === factsRevision &&
|
|
1356
|
+
materializedDigest === getMeta(db, "apexConsumerMaterializationDigest") &&
|
|
1357
|
+
materializedSites === expectedSites &&
|
|
1358
|
+
invalidDescriptorFiles === 0 &&
|
|
1359
|
+
missingDescriptorFiles === 0 &&
|
|
1360
|
+
missingInterfaceBindings.size === 0);
|
|
1361
|
+
const where = [
|
|
1362
|
+
"u.status='resolved'",
|
|
1363
|
+
"u.targetIdentity IS NOT NULL",
|
|
1364
|
+
"u.targetFile=?",
|
|
1365
|
+
"(lower(u.targetOwner)=lower(?) OR substr(lower(u.targetOwner),1,length(?)+1)=lower(?)||'.')",
|
|
1366
|
+
"u.filePath<>?",
|
|
1367
|
+
"u.isTest IS NOT NULL",
|
|
1368
|
+
];
|
|
1369
|
+
const params = [
|
|
1370
|
+
selected.filePath,
|
|
1371
|
+
targetOwner,
|
|
1372
|
+
targetOwner,
|
|
1373
|
+
targetOwner,
|
|
1374
|
+
selected.filePath,
|
|
1375
|
+
];
|
|
1376
|
+
if (!bound)
|
|
1377
|
+
where.push("0");
|
|
1378
|
+
if (request.testScope !== "all") {
|
|
1379
|
+
where.push("u.isTest=?");
|
|
1380
|
+
params.push(request.testScope === "test" ? 1 : 0);
|
|
1381
|
+
}
|
|
1382
|
+
if (filterPath) {
|
|
1383
|
+
where.push("(u.filePath=? OR substr(u.filePath,1,length(?)+1)=?||'/')");
|
|
1384
|
+
params.push(filterPath, filterPath, filterPath);
|
|
1385
|
+
}
|
|
1386
|
+
const from = "FROM apex_class_uses u JOIN index_state i ON i.filePath=u.filePath JOIN apex_consumer_files f ON f.filePath=u.filePath";
|
|
1387
|
+
const predicate = where.join(" AND ");
|
|
1388
|
+
const totals = tablesPresent
|
|
1389
|
+
? db
|
|
1390
|
+
.query(`SELECT COUNT(*) count,COUNT(DISTINCT u.filePath) files ${from} WHERE ${predicate}`)
|
|
1391
|
+
.get(...params)
|
|
1392
|
+
: undefined;
|
|
1393
|
+
const targetInternalSites = tablesPresent
|
|
1394
|
+
? (db
|
|
1395
|
+
.query("SELECT COUNT(*) count FROM apex_class_uses WHERE status='resolved' AND filePath=? AND targetFile=? AND (lower(targetOwner)=lower(?) OR substr(lower(targetOwner),1,length(?)+1)=lower(?)||'.')")
|
|
1396
|
+
.get(selected.filePath, selected.filePath, targetOwner, targetOwner, targetOwner)
|
|
1397
|
+
?.count ?? 0)
|
|
1398
|
+
: 0;
|
|
1399
|
+
const pageParams = [...params];
|
|
1400
|
+
let afterSql = "";
|
|
1401
|
+
if (after) {
|
|
1402
|
+
afterSql =
|
|
1403
|
+
" AND (u.filePath COLLATE BINARY,u.startByte,u.endByte,u.kind COLLATE BINARY)>(? COLLATE BINARY,?,?,? COLLATE BINARY)";
|
|
1404
|
+
pageParams.push(after.file, after.startByte, after.endByte, after.kind);
|
|
1405
|
+
}
|
|
1406
|
+
pageParams.push(request.limit + 1);
|
|
1407
|
+
const rows = tablesPresent
|
|
1408
|
+
? db
|
|
1409
|
+
.query(`SELECT u.* ${from} WHERE ${predicate}${afterSql} ORDER BY u.filePath COLLATE BINARY,u.startByte,u.endByte,u.kind COLLATE BINARY LIMIT ?`)
|
|
1410
|
+
.all(...pageParams)
|
|
1411
|
+
: [];
|
|
1412
|
+
const storedByFile = new Map(stored.map((row) => [row.filePath, row]));
|
|
1413
|
+
const pageFacts = new Map();
|
|
1414
|
+
const proofByFile = new Map();
|
|
1415
|
+
let invalidRows = false;
|
|
1416
|
+
const results = [];
|
|
1417
|
+
for (const row of rows.slice(0, request.limit)) {
|
|
1418
|
+
let site;
|
|
1419
|
+
try {
|
|
1420
|
+
site = JSON.parse(row.raw);
|
|
1421
|
+
}
|
|
1422
|
+
catch {
|
|
1423
|
+
invalidRows = true;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (!validConsumerSite(site) ||
|
|
1427
|
+
site.blockedReason ||
|
|
1428
|
+
site.start.byte !== row.startByte ||
|
|
1429
|
+
site.end.byte !== row.endByte ||
|
|
1430
|
+
site.kind !== row.kind) {
|
|
1431
|
+
invalidRows = true;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (!pageFacts.has(row.filePath)) {
|
|
1435
|
+
const descriptor = storedByFile.get(row.filePath) ??
|
|
1436
|
+
(fastProof
|
|
1437
|
+
? db
|
|
1438
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath WHERE f.filePath=?")
|
|
1439
|
+
.get(row.filePath)
|
|
1440
|
+
: null);
|
|
1441
|
+
pageFacts.set(row.filePath, descriptor ? readApexConsumerFacts(descriptor) : null);
|
|
1442
|
+
}
|
|
1443
|
+
const facts = pageFacts.get(row.filePath);
|
|
1444
|
+
if (!facts || facts.isTest !== (row.isTest === 1)) {
|
|
1445
|
+
invalidRows = true;
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
let proofs = proofByFile.get(row.filePath);
|
|
1449
|
+
if (!proofs) {
|
|
1450
|
+
proofs = resolveApexClassUses(catalog, facts);
|
|
1451
|
+
proofByFile.set(row.filePath, proofs);
|
|
1452
|
+
}
|
|
1453
|
+
const proof = proofs.find((entry) => entry.status === "resolved" && JSON.stringify(entry.site) === row.raw);
|
|
1454
|
+
if (!proof?.target ||
|
|
1455
|
+
proof.target.file !== selected.filePath ||
|
|
1456
|
+
!(proof.target.declaration.ownerPath.toLowerCase() === targetOwner.toLowerCase() ||
|
|
1457
|
+
proof.target.declaration.ownerPath
|
|
1458
|
+
.toLowerCase()
|
|
1459
|
+
.startsWith(`${targetOwner.toLowerCase()}.`))) {
|
|
1460
|
+
invalidRows = true;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const owners = allClassRows.filter((candidate) => candidate.filePath === row.filePath &&
|
|
1464
|
+
typeOwner(candidate)?.toLowerCase() === site.lexicalOwner?.toLowerCase());
|
|
1465
|
+
const ownerIdentity = owners.length === 1 ? (owners[0].identity ?? null) : null;
|
|
1466
|
+
results.push({
|
|
1467
|
+
key: { file: row.filePath, startByte: row.startByte, endByte: row.endByte, kind: row.kind },
|
|
1468
|
+
file: row.filePath,
|
|
1469
|
+
start: { line: site.start.line, column: site.start.column, byte: site.start.byte },
|
|
1470
|
+
end: { line: site.end.line, column: site.end.column, byte: site.end.byte },
|
|
1471
|
+
kind: site.kind,
|
|
1472
|
+
qualifiedSpelling: site.qualifiedSpelling,
|
|
1473
|
+
ownerIdentity,
|
|
1474
|
+
isTest: row.isTest === 1,
|
|
1475
|
+
grounding: "syntax-resolved",
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
if (invalidRows)
|
|
1479
|
+
results.length = 0;
|
|
1480
|
+
const files = [...new Set(results.map((site) => site.file))].map((file) => ({
|
|
1481
|
+
file,
|
|
1482
|
+
isTest: results.find((site) => site.file === file)?.isTest ?? false,
|
|
1483
|
+
siteCount: db
|
|
1484
|
+
.query(`SELECT COUNT(*) count ${from} WHERE ${predicate} AND u.filePath=?`)
|
|
1485
|
+
.get(...params, file)?.count ?? 0,
|
|
1486
|
+
returnedSiteCount: results.filter((site) => site.file === file).length,
|
|
1487
|
+
}));
|
|
1488
|
+
const endStamp = pinnedStamp ? apexConsumerSnapshotStamp(db, true) : null;
|
|
1489
|
+
if (pinnedStamp && endStamp !== pinnedStamp) {
|
|
1490
|
+
throw new ClassConsumerQueryError("CLASS_CONSUMER_STALE_CURSOR", "The graph changed while reading its snapshot; retry the query.");
|
|
1491
|
+
}
|
|
1492
|
+
if (bound && !invalidRows && pinnedStamp && beforeStamp === pinnedStamp)
|
|
1493
|
+
publishProof = { stamp: pinnedStamp, descriptorDigest, materializedDigest, validationDigest };
|
|
1494
|
+
completed = true;
|
|
1495
|
+
return {
|
|
1496
|
+
targetIdentity,
|
|
1497
|
+
targetKind,
|
|
1498
|
+
queryDigest,
|
|
1499
|
+
snapshotDigest,
|
|
1500
|
+
count: invalidRows ? 0 : (totals?.count ?? 0),
|
|
1501
|
+
consumerFileCount: invalidRows ? 0 : (totals?.files ?? 0),
|
|
1502
|
+
returnedCount: results.length,
|
|
1503
|
+
results,
|
|
1504
|
+
files,
|
|
1505
|
+
hasMore: !invalidRows && rows.length > request.limit,
|
|
1506
|
+
analysis: {
|
|
1507
|
+
version: 1,
|
|
1508
|
+
grounding: "syntax-resolved",
|
|
1509
|
+
partial: !bound ||
|
|
1510
|
+
missingDescriptorFiles > 0 ||
|
|
1511
|
+
partialFiles > 0 ||
|
|
1512
|
+
invalidRows ||
|
|
1513
|
+
unresolvedSites > 0 ||
|
|
1514
|
+
unsupportedUses > 0,
|
|
1515
|
+
inspectedFiles: validation.validFiles,
|
|
1516
|
+
missingDescriptorFiles: missingDescriptorFiles + missingInterfaceBindings.size,
|
|
1517
|
+
invalidDescriptorFiles: invalidDescriptorFiles + (invalidRows ? 1 : 0),
|
|
1518
|
+
unresolvedSites,
|
|
1519
|
+
unsupportedUses,
|
|
1520
|
+
exclusions: { targetInternalSites },
|
|
1521
|
+
limitations: [
|
|
1522
|
+
"Only the nine supported explicit Apex syntax roles are resolved; this is not runtime reachability or complete class-use coverage.",
|
|
1523
|
+
"Descriptor, unresolved-site and unsupported-use counters cover the inspected indexed Apex inventory, not just this target.",
|
|
1524
|
+
"Inherited or ambiguous value/type scopes can remain unresolved; target self-file sites are excluded.",
|
|
1525
|
+
"Query results cover this repository's indexed graph scope only, not federated repositories or metadata.",
|
|
1526
|
+
],
|
|
1527
|
+
},
|
|
1528
|
+
indexCoverage,
|
|
1529
|
+
freshness,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
finally {
|
|
1533
|
+
releaseApexConsumerRead(db, beforeStamp !== null, completed, publishProof);
|
|
1534
|
+
}
|
|
683
1535
|
}
|
|
684
1536
|
function resolveSymbolRows(db, repoPath, name, selector = {}) {
|
|
685
1537
|
let rows = db.query("SELECT * FROM symbols WHERE name = ?").all(name);
|
|
@@ -1278,7 +2130,8 @@ export function resetSearchCacheStats() {
|
|
|
1278
2130
|
}
|
|
1279
2131
|
}
|
|
1280
2132
|
/**
|
|
1281
|
-
* Decode a repository's
|
|
2133
|
+
* Decode a repository's symbol metadata once per index generation, including
|
|
2134
|
+
* structural-only symbols. Semantic coverage must not gate lexical retrieval.
|
|
1282
2135
|
* Corrupt vectors remain represented so totals and FTS behavior stay truthful,
|
|
1283
2136
|
* but exact vector scoring can skip them without reparsing every warm query.
|
|
1284
2137
|
*/
|
|
@@ -1294,12 +2147,14 @@ function getSearchMetadataSnapshot(repoPath, db) {
|
|
|
1294
2147
|
const stmt = db.query(`
|
|
1295
2148
|
SELECT s.id, s.identity, s.name, s.kind, s.filePath, s.startLine, s.endLine, se.embedding
|
|
1296
2149
|
FROM symbols s
|
|
1297
|
-
JOIN symbol_embeddings se ON s.id = se.symbolId
|
|
2150
|
+
LEFT JOIN symbol_embeddings se ON s.id = se.symbolId
|
|
1298
2151
|
`);
|
|
1299
2152
|
const raw = stmt.all();
|
|
1300
2153
|
stmt.finalize();
|
|
1301
2154
|
return raw.map((row) => {
|
|
1302
2155
|
const { embedding, ...metadata } = row;
|
|
2156
|
+
if (embedding === null)
|
|
2157
|
+
return { ...metadata, vector: null };
|
|
1303
2158
|
try {
|
|
1304
2159
|
return { ...metadata, vector: bufferToFloat32(embedding) };
|
|
1305
2160
|
}
|
|
@@ -2526,14 +3381,17 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
|
|
|
2526
3381
|
}
|
|
2527
3382
|
}
|
|
2528
3383
|
else if (isApex) {
|
|
2529
|
-
if (node.type === "class_declaration") {
|
|
3384
|
+
if (node.type === "class_declaration" || node.type === "interface_declaration") {
|
|
2530
3385
|
const nameNode = node.childForFieldName("name") || node.children.find((c) => c.type === "identifier");
|
|
2531
3386
|
if (nameNode) {
|
|
2532
3387
|
const name = nameNode.text;
|
|
2533
3388
|
symName = name;
|
|
2534
3389
|
definitions.push({
|
|
3390
|
+
...(node.type === "class_declaration"
|
|
3391
|
+
? { apexDeclaration: JSON.stringify(apexDeclaration(node)) }
|
|
3392
|
+
: {}),
|
|
2535
3393
|
name,
|
|
2536
|
-
kind: "class",
|
|
3394
|
+
kind: node.type === "class_declaration" ? "class" : "interface",
|
|
2537
3395
|
startLine: node.startPosition.row + 1,
|
|
2538
3396
|
endLine: node.endPosition.row + 1,
|
|
2539
3397
|
startCol: node.startPosition.column,
|
|
@@ -2598,6 +3456,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
|
|
|
2598
3456
|
symName = name;
|
|
2599
3457
|
isNewSymbol = true;
|
|
2600
3458
|
definitions.push({
|
|
3459
|
+
apexDeclaration: JSON.stringify(apexDeclaration(node)),
|
|
2601
3460
|
name,
|
|
2602
3461
|
kind: "method",
|
|
2603
3462
|
startLine: node.startPosition.row + 1,
|
|
@@ -2768,6 +3627,15 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
|
|
|
2768
3627
|
}
|
|
2769
3628
|
// --- Identify Dependencies (Imports) ---
|
|
2770
3629
|
if (!isPython && !isApex) {
|
|
3630
|
+
// A source-bearing re-export is a real module dependency even though it
|
|
3631
|
+
// introduces no local binding. The AST source field covers named,
|
|
3632
|
+
// aliased, namespace and star exports without mistaking strings or
|
|
3633
|
+
// comments for wiring. Local `export { value }` has no source field.
|
|
3634
|
+
if (node.type === "export_statement") {
|
|
3635
|
+
const sourceNode = node.childForFieldName("source");
|
|
3636
|
+
if (sourceNode?.type === "string")
|
|
3637
|
+
fileDependencies.push(sourceNode.text.replace(/^['"`]|['"`]$/g, ""));
|
|
3638
|
+
}
|
|
2771
3639
|
if (node.type === "import_statement" || node.type === "import_declaration") {
|
|
2772
3640
|
const sourceNode = node.childForFieldName("source");
|
|
2773
3641
|
if (sourceNode?.type === "string") {
|
|
@@ -2847,6 +3715,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
|
|
|
2847
3715
|
if (nameNode) {
|
|
2848
3716
|
const calleeName = nameNode.text;
|
|
2849
3717
|
references.push({
|
|
3718
|
+
apexCall: node.childForFieldName("object") ? JSON.stringify(apexCall(node)) : undefined,
|
|
2850
3719
|
callerSymbol: getEnclosingSymbol(),
|
|
2851
3720
|
calleeSymbol: calleeName,
|
|
2852
3721
|
line: node.startPosition.row + 1,
|
|
@@ -2860,6 +3729,7 @@ function extractSymbolsAndReferences(rootNode, isPython, isApex = false, filePat
|
|
|
2860
3729
|
if (typeNode) {
|
|
2861
3730
|
const calleeName = typeNode.text;
|
|
2862
3731
|
references.push({
|
|
3732
|
+
apexCall: JSON.stringify(apexCall(node)),
|
|
2863
3733
|
callerSymbol: getEnclosingSymbol(),
|
|
2864
3734
|
calleeSymbol: calleeName,
|
|
2865
3735
|
line: node.startPosition.row + 1,
|
|
@@ -3453,19 +4323,25 @@ const packageMapCache = new Map();
|
|
|
3453
4323
|
function invalidateResolutionCaches(repoPath) {
|
|
3454
4324
|
const resolved = path.resolve(repoPath);
|
|
3455
4325
|
packageMapCache.delete(resolved);
|
|
4326
|
+
salesforceBindingTargetCache.delete(resolved);
|
|
3456
4327
|
const prefix = `${resolved}\0`;
|
|
3457
4328
|
for (const key of reExportCache.keys())
|
|
3458
4329
|
if (key.startsWith(prefix))
|
|
3459
4330
|
reExportCache.delete(key);
|
|
3460
4331
|
}
|
|
4332
|
+
const explicitPackageInputs = new AsyncLocalStorage();
|
|
3461
4333
|
function getPackageMap(repoPath) {
|
|
3462
4334
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
3463
4335
|
const cached = packageMapCache.get(resolvedRepoPath);
|
|
3464
|
-
|
|
4336
|
+
const explicitInputs = explicitPackageInputs.getStore() ?? [];
|
|
4337
|
+
if (!explicitInputs.length && cached?.generation === indexGeneration)
|
|
3465
4338
|
return cached.value;
|
|
3466
4339
|
const map = new Map();
|
|
3467
4340
|
let cacheable = true;
|
|
3468
|
-
for (const rel of
|
|
4341
|
+
for (const rel of new Set([
|
|
4342
|
+
...explicitInputs,
|
|
4343
|
+
...listSourceFiles(repoPath, (file) => path.posix.basename(file) === "package.json"),
|
|
4344
|
+
])) {
|
|
3469
4345
|
try {
|
|
3470
4346
|
const manifest = readWholeSource(repoPath, rel);
|
|
3471
4347
|
if (manifest.availability.state !== "available")
|
|
@@ -3485,7 +4361,7 @@ function getPackageMap(repoPath) {
|
|
|
3485
4361
|
cacheable = false;
|
|
3486
4362
|
}
|
|
3487
4363
|
}
|
|
3488
|
-
if (cacheable)
|
|
4364
|
+
if (cacheable && !explicitInputs.length)
|
|
3489
4365
|
packageMapCache.set(resolvedRepoPath, {
|
|
3490
4366
|
generation: indexGeneration,
|
|
3491
4367
|
value: map,
|
|
@@ -3637,10 +4513,7 @@ function getLineAndColumnForIndex(content, index) {
|
|
|
3637
4513
|
return { line, column };
|
|
3638
4514
|
}
|
|
3639
4515
|
function findClassFilePath(className, repoPath) {
|
|
3640
|
-
return (
|
|
3641
|
-
accept: (file) => path.posix.basename(file) === `${className}.cls`,
|
|
3642
|
-
limit: 1,
|
|
3643
|
-
})[0] ?? null);
|
|
4516
|
+
return (listSourceFiles(repoPath, (file) => path.posix.basename(file) === `${className}.cls`)[0] ?? null);
|
|
3644
4517
|
}
|
|
3645
4518
|
/**
|
|
3646
4519
|
* C19 only reports an Apex bridge when its class path is unambiguous. The
|
|
@@ -3773,6 +4646,9 @@ function salesforceMetadataFile(relativePath) {
|
|
|
3773
4646
|
const root = "force-app/main/default/";
|
|
3774
4647
|
if (!normalized.startsWith(root))
|
|
3775
4648
|
return null;
|
|
4649
|
+
const webLink = /^force-app\/main\/default\/objects\/([A-Za-z]\w*)\/webLinks\/([A-Za-z]\w*)\.webLink-meta\.xml$/.exec(normalized);
|
|
4650
|
+
if (webLink)
|
|
4651
|
+
return { type: "weblink", object: webLink[1], name: webLink[2] };
|
|
3776
4652
|
let match = /^force-app\/main\/default\/objects\/([A-Za-z]\w*)\/\1\.object-meta\.xml$/.exec(normalized);
|
|
3777
4653
|
if (match)
|
|
3778
4654
|
return { type: "object", object: match[1] };
|
|
@@ -3787,6 +4663,13 @@ function salesforceMetadataFile(relativePath) {
|
|
|
3787
4663
|
match = /^force-app\/main\/default\/permissionSets\/([A-Za-z]\w*)\.permissionSet-meta\.xml$/.exec(normalized);
|
|
3788
4664
|
if (match)
|
|
3789
4665
|
return { type: "permissionSet", name: match[1] };
|
|
4666
|
+
match =
|
|
4667
|
+
/^force-app\/main\/default\/quickActions\/([A-Za-z]\w*(?:\.[A-Za-z]\w*)?)\.quickAction-meta\.xml$/.exec(normalized);
|
|
4668
|
+
if (match)
|
|
4669
|
+
return { type: "quickAction", name: match[1] };
|
|
4670
|
+
match = /^force-app\/main\/default\/flexipages\/([A-Za-z]\w*)\.flexipage-meta\.xml$/.exec(normalized);
|
|
4671
|
+
if (match)
|
|
4672
|
+
return { type: "flexipage", name: match[1] };
|
|
3790
4673
|
match = /^force-app\/main\/default\/flows\/([A-Za-z]\w*)\.flow-meta\.xml$/.exec(normalized);
|
|
3791
4674
|
if (match)
|
|
3792
4675
|
return { type: "flow", name: match[1] };
|
|
@@ -3810,11 +4693,12 @@ function findUniqueInvocableApexMethod(className, methodName, repoPath) {
|
|
|
3810
4693
|
const source = read.text;
|
|
3811
4694
|
const matches = [];
|
|
3812
4695
|
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)) {
|
|
3813
|
-
if (!isExecutablePosition(source, match.index ?? 0) ||
|
|
4696
|
+
if (!isExecutablePosition(source, match.index ?? 0) ||
|
|
4697
|
+
(methodName !== undefined && match[1] !== methodName))
|
|
3814
4698
|
continue;
|
|
3815
4699
|
matches.push({
|
|
3816
4700
|
file,
|
|
3817
|
-
method:
|
|
4701
|
+
method: match[1],
|
|
3818
4702
|
line: source.slice(0, match.index ?? 0).split("\n").length,
|
|
3819
4703
|
evidence: match[0].trim(),
|
|
3820
4704
|
});
|
|
@@ -4009,7 +4893,9 @@ export const SALESFORCE_METADATA_KINDS = [
|
|
|
4009
4893
|
"salesforce_field",
|
|
4010
4894
|
"salesforce_record_type",
|
|
4011
4895
|
"salesforce_permission_set",
|
|
4896
|
+
"salesforce_quick_action",
|
|
4012
4897
|
"salesforce_flow",
|
|
4898
|
+
"salesforce_weblink",
|
|
4013
4899
|
"salesforce_workflow",
|
|
4014
4900
|
"salesforce_approval",
|
|
4015
4901
|
];
|
|
@@ -4018,7 +4904,10 @@ const SALESFORCE_KIND_BY_TYPE = {
|
|
|
4018
4904
|
field: "salesforce_field",
|
|
4019
4905
|
recordType: "salesforce_record_type",
|
|
4020
4906
|
permissionSet: "salesforce_permission_set",
|
|
4907
|
+
quickAction: "salesforce_quick_action",
|
|
4908
|
+
flexipage: "salesforce_flexipage",
|
|
4021
4909
|
flow: "salesforce_flow",
|
|
4910
|
+
weblink: "salesforce_weblink",
|
|
4022
4911
|
workflow: "salesforce_workflow",
|
|
4023
4912
|
approval: "salesforce_approval",
|
|
4024
4913
|
};
|
|
@@ -4058,7 +4947,9 @@ function describeSalesforceMetadata(metadata, content) {
|
|
|
4058
4947
|
const label = first("label") ?? first("masterLabel");
|
|
4059
4948
|
const name = metadata.type === "flow"
|
|
4060
4949
|
? metadata.name
|
|
4061
|
-
: metadata.type === "permissionSet"
|
|
4950
|
+
: metadata.type === "permissionSet" ||
|
|
4951
|
+
metadata.type === "quickAction" ||
|
|
4952
|
+
metadata.type === "flexipage"
|
|
4062
4953
|
? metadata.name
|
|
4063
4954
|
: metadata.type === "recordType"
|
|
4064
4955
|
? `${metadata.object}.${metadata.recordType}`
|
|
@@ -4085,6 +4976,20 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
4085
4976
|
add(qualified, kind, evidence);
|
|
4086
4977
|
};
|
|
4087
4978
|
switch (metadata.type) {
|
|
4979
|
+
case "flexipage": {
|
|
4980
|
+
for (const entry of await resolvedSalesforceComponentBindings(content, "flexipage", repoPath))
|
|
4981
|
+
add(entry.to, entry.kind, entry.sourceEvidence);
|
|
4982
|
+
break;
|
|
4983
|
+
}
|
|
4984
|
+
case "quickAction": {
|
|
4985
|
+
const component = quickActionComponent(content);
|
|
4986
|
+
if (component) {
|
|
4987
|
+
const target = findUniqueSalesforceBindingTarget(component.name, repoPath, "aura");
|
|
4988
|
+
if (target)
|
|
4989
|
+
add(target, "quickaction_lightning_component", component.evidence);
|
|
4990
|
+
}
|
|
4991
|
+
break;
|
|
4992
|
+
}
|
|
4088
4993
|
case "object":
|
|
4089
4994
|
add(metadata.object, "salesforce_metadata_object", "Salesforce DX CustomObject file path");
|
|
4090
4995
|
break;
|
|
@@ -4142,7 +5047,7 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
4142
5047
|
if (!/<actionType>apex<\/actionType>/.test(action[1]))
|
|
4143
5048
|
continue;
|
|
4144
5049
|
for (const entry of staticXmlTagValues(action[1], "actionName")) {
|
|
4145
|
-
const target = /^([A-Za-z]\w*)
|
|
5050
|
+
const target = /^([A-Za-z]\w*)(?:\.([A-Za-z]\w*))?$/.exec(entry.value);
|
|
4146
5051
|
if (!target)
|
|
4147
5052
|
continue;
|
|
4148
5053
|
const invocable = findUniqueInvocableApexMethod(target[1], target[2], repoPath);
|
|
@@ -4159,16 +5064,14 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
4159
5064
|
});
|
|
4160
5065
|
}
|
|
4161
5066
|
}
|
|
4162
|
-
for (const
|
|
4163
|
-
|
|
4164
|
-
if (!component)
|
|
4165
|
-
continue;
|
|
4166
|
-
const target = findSalesforceLwcComponentFile(component[1], repoPath);
|
|
4167
|
-
if (target)
|
|
4168
|
-
add(target, "flow_screen_component", entry.evidence);
|
|
4169
|
-
}
|
|
5067
|
+
for (const binding of await resolvedSalesforceComponentBindings(content, "flow", repoPath))
|
|
5068
|
+
add(binding.to, binding.kind, binding.sourceEvidence);
|
|
4170
5069
|
break;
|
|
4171
5070
|
}
|
|
5071
|
+
case "weblink":
|
|
5072
|
+
for (const binding of await resolvedSalesforceComponentBindings(content, "weblink", repoPath))
|
|
5073
|
+
add(binding.to, binding.kind, binding.sourceEvidence);
|
|
5074
|
+
break;
|
|
4172
5075
|
case "workflow":
|
|
4173
5076
|
add(metadata.object, "workflow_object", "Salesforce DX workflow object file path");
|
|
4174
5077
|
for (const entry of staticXmlTagValues(content, "field"))
|
|
@@ -4224,21 +5127,255 @@ async function indexSalesforceMetadataFile(content, relativePath, repoPath, db,
|
|
|
4224
5127
|
throw error;
|
|
4225
5128
|
}
|
|
4226
5129
|
}
|
|
5130
|
+
/** Mask non-markup content without interpreting entities or loading external resources. */
|
|
5131
|
+
function salesforceBindingMarkup(content) {
|
|
5132
|
+
return content.replace(/<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>/g, (value) => value.replace(/[^\n]/g, " "));
|
|
5133
|
+
}
|
|
5134
|
+
function quickActionComponent(content) {
|
|
5135
|
+
const markup = salesforceBindingMarkup(content);
|
|
5136
|
+
if (!/^\s*(?:<\?xml[^?]*\?>\s*)?<QuickAction(?:\s[^<>]*)?>/.test(markup))
|
|
5137
|
+
return null;
|
|
5138
|
+
const types = staticXmlTagValues(markup, "type");
|
|
5139
|
+
const components = staticXmlTagValues(markup, "lightningComponent");
|
|
5140
|
+
if (types.length !== 1 || types[0].value !== "LightningComponent" || components.length !== 1)
|
|
5141
|
+
return null;
|
|
5142
|
+
const name = /^(?:c:)?([A-Za-z]\w*)$/.exec(components[0].value)?.[1];
|
|
5143
|
+
return name && !name.includes("__") ? { name, evidence: components[0].evidence } : null;
|
|
5144
|
+
}
|
|
5145
|
+
const salesforceBindingTargetCache = new Map();
|
|
5146
|
+
/** One source-path catalog per graph generation, never a walk for each binding. */
|
|
5147
|
+
function salesforceBindingTargets(repoPath) {
|
|
5148
|
+
const key = path.resolve(repoPath);
|
|
5149
|
+
const coverageDigest = indexCoverageSummary(coverageForDatabase(extractionCoverage.getStore() ?? dbInstances.get(key))).digest;
|
|
5150
|
+
let cached = salesforceBindingTargetCache.get(key);
|
|
5151
|
+
if (!cached ||
|
|
5152
|
+
cached.generation !== indexGeneration ||
|
|
5153
|
+
cached.coverageDigest !== coverageDigest) {
|
|
5154
|
+
const targets = new Map();
|
|
5155
|
+
const files = listSourceFiles(repoPath, (file) => file.endsWith(".cls") ||
|
|
5156
|
+
file.endsWith(".cmp") ||
|
|
5157
|
+
file.endsWith(".js") ||
|
|
5158
|
+
file.endsWith(".app") ||
|
|
5159
|
+
file.endsWith(".page"));
|
|
5160
|
+
for (const file of files) {
|
|
5161
|
+
const name = file.endsWith(".cls")
|
|
5162
|
+
? path.posix.basename(file, ".cls")
|
|
5163
|
+
: /(?:^|\/)aura\/([A-Za-z]\w*)\/\1\.cmp$/.exec(file)?.[1];
|
|
5164
|
+
if (!name)
|
|
5165
|
+
continue;
|
|
5166
|
+
const targetKey = `${file.endsWith(".cls") ? "apex" : "aura"}:${name}`;
|
|
5167
|
+
const files = targets.get(targetKey) ?? [];
|
|
5168
|
+
if (files.length < 2)
|
|
5169
|
+
files.push(file);
|
|
5170
|
+
targets.set(targetKey, files);
|
|
5171
|
+
}
|
|
5172
|
+
cached = {
|
|
5173
|
+
generation: indexGeneration,
|
|
5174
|
+
coverageDigest,
|
|
5175
|
+
targets,
|
|
5176
|
+
components: componentCatalog(files),
|
|
5177
|
+
};
|
|
5178
|
+
if (salesforceBindingTargetCache.size >= 8)
|
|
5179
|
+
salesforceBindingTargetCache.clear();
|
|
5180
|
+
salesforceBindingTargetCache.set(key, cached);
|
|
5181
|
+
}
|
|
5182
|
+
return cached;
|
|
5183
|
+
}
|
|
5184
|
+
function findUniqueSalesforceBindingTarget(name, repoPath, kind) {
|
|
5185
|
+
const matches = salesforceBindingTargets(repoPath).targets.get(`${kind}:${name}`) ?? [];
|
|
5186
|
+
return matches.length === 1 ? matches[0] : null;
|
|
5187
|
+
}
|
|
5188
|
+
/** Reuse the shipped XML grammar; HTML deliberately retains its own literal subset. */
|
|
5189
|
+
async function salesforceComponentBindings(content, format) {
|
|
5190
|
+
if (format === "lwc")
|
|
5191
|
+
return componentBindings(content, format);
|
|
5192
|
+
const language = await getLanguageForFile("component.xml");
|
|
5193
|
+
if (!language)
|
|
5194
|
+
return [];
|
|
5195
|
+
const parser = new Parser();
|
|
5196
|
+
try {
|
|
5197
|
+
parser.setLanguage(language);
|
|
5198
|
+
return componentBindings(content, format, (source) => {
|
|
5199
|
+
const tree = parser.parse(source);
|
|
5200
|
+
try {
|
|
5201
|
+
return !tree.rootNode.hasError();
|
|
5202
|
+
}
|
|
5203
|
+
finally {
|
|
5204
|
+
tree.delete();
|
|
5205
|
+
}
|
|
5206
|
+
});
|
|
5207
|
+
}
|
|
5208
|
+
finally {
|
|
5209
|
+
parser.delete();
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
async function resolvedSalesforceComponentBindings(content, format, repoPath) {
|
|
5213
|
+
const bindings = await salesforceComponentBindings(content, format);
|
|
5214
|
+
if (!bindings.length)
|
|
5215
|
+
return [];
|
|
5216
|
+
const catalog = salesforceBindingTargets(repoPath).components;
|
|
5217
|
+
const seen = new Set();
|
|
5218
|
+
return bindings.flatMap((binding) => {
|
|
5219
|
+
const target = catalog.resolve(binding);
|
|
5220
|
+
if (!target)
|
|
5221
|
+
return [];
|
|
5222
|
+
const key = `${target.file}\0${binding.kind}\0${binding.evidence}`;
|
|
5223
|
+
if (seen.has(key))
|
|
5224
|
+
return [];
|
|
5225
|
+
seen.add(key);
|
|
5226
|
+
return [
|
|
5227
|
+
{
|
|
5228
|
+
to: target.file,
|
|
5229
|
+
kind: binding.kind === "aura_component" && target.kind === "lwc"
|
|
5230
|
+
? "aura_lwc_component"
|
|
5231
|
+
: binding.kind,
|
|
5232
|
+
sourceEvidence: binding.evidence,
|
|
5233
|
+
},
|
|
5234
|
+
];
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
/** Literal design-time Apex data sources, not method execution or runtime dispatch. */
|
|
5238
|
+
function appBuilderApexDataSources(content, aura) {
|
|
5239
|
+
const markup = salesforceBindingMarkup(content);
|
|
5240
|
+
const root = aura ? "design:component" : "LightningComponentBundle";
|
|
5241
|
+
if (!new RegExp(String.raw `^\s*(?:<\?xml[^?]*\?>\s*)?<${root}(?:\s[^<>]*)?>`).test(markup))
|
|
5242
|
+
return [];
|
|
5243
|
+
const regions = aura
|
|
5244
|
+
? [markup]
|
|
5245
|
+
: [...markup.matchAll(/<targetConfig\b[^<>]*>([\s\S]*?)<\/targetConfig>/g)].map((match) => match[1]);
|
|
5246
|
+
const results = [];
|
|
5247
|
+
const tag = aura ? "design:attribute" : "property";
|
|
5248
|
+
for (const region of regions) {
|
|
5249
|
+
for (const match of region.matchAll(new RegExp(String.raw `<${tag}\b(?:[^"'<>]|"[^"]*"|'[^']*')*\/?>`, "g"))) {
|
|
5250
|
+
const attributes = [
|
|
5251
|
+
...match[0].matchAll(/([A-Za-z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g),
|
|
5252
|
+
];
|
|
5253
|
+
const sources = attributes.filter((attribute) => attribute[1] === "datasource");
|
|
5254
|
+
if (sources.length !== 1)
|
|
5255
|
+
continue;
|
|
5256
|
+
const name = /^apex:\/\/([A-Za-z]\w*)$/.exec(sources[0][2] ?? sources[0][3])?.[1];
|
|
5257
|
+
if (name && !name.includes("__"))
|
|
5258
|
+
results.push({ name, evidence: sources[0][0] });
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
return results;
|
|
5262
|
+
}
|
|
5263
|
+
/** Re-evaluate indexed bindings, including previously missing/ambiguous targets. */
|
|
5264
|
+
async function refreshSalesforceMetadataBindings(db, repoPath, changedFiles) {
|
|
5265
|
+
const classes = new Set(changedFiles.filter((file) => file.endsWith(".cls")).map((file) => path.basename(file, ".cls")));
|
|
5266
|
+
const components = new Set(changedFiles.filter((file) => file.endsWith(".cmp")).map((file) => path.basename(file, ".cmp")));
|
|
5267
|
+
const componentNames = new Set(changedFiles.flatMap((file) => {
|
|
5268
|
+
const name = /^force-app\/main\/default\/(?:aura|lwc)\/([A-Za-z]\w*)\/\1\.(?:cmp|js)$/.exec(file)?.[1];
|
|
5269
|
+
return name ? [name] : [];
|
|
5270
|
+
}));
|
|
5271
|
+
const componentTags = new Set([...componentNames].map((name) => name.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)));
|
|
5272
|
+
const applicationNames = new Set(changedFiles.flatMap((file) => /^force-app\/main\/default\/aura\/([A-Za-z]\w*)\/\1\.app$/.exec(file)?.slice(1) ?? []));
|
|
5273
|
+
const pageNames = new Set(changedFiles.flatMap((file) => /^force-app\/main\/default\/pages\/([A-Za-z]\w*)\.page$/.exec(file)?.slice(1) ?? []));
|
|
5274
|
+
if (!classes.size &&
|
|
5275
|
+
!components.size &&
|
|
5276
|
+
!componentNames.size &&
|
|
5277
|
+
!applicationNames.size &&
|
|
5278
|
+
!pageNames.size)
|
|
5279
|
+
return [];
|
|
5280
|
+
// Target additions/deletions can precede the generation increment. Refresh
|
|
5281
|
+
// the catalog before reconsidering formerly missing or ambiguous bindings.
|
|
5282
|
+
salesforceBindingTargetCache.delete(path.resolve(repoPath));
|
|
5283
|
+
const candidates = db
|
|
5284
|
+
.query("SELECT filePath FROM index_state WHERE filePath LIKE 'force-app/main/default/quickActions/%.quickAction-meta.xml' OR filePath LIKE 'force-app/main/default/lwc/%.js-meta.xml' OR filePath LIKE 'force-app/main/default/aura/%.design' OR filePath LIKE 'force-app/main/default/aura/%.cmp' OR filePath LIKE 'force-app/main/default/aura/%.app' OR filePath LIKE 'force-app/main/default/lwc/%.html' OR filePath LIKE 'force-app/main/default/flexipages/%.flexipage-meta.xml' OR filePath LIKE 'force-app/main/default/flows/%.flow-meta.xml' OR filePath LIKE 'force-app/main/default/pages/%.page' OR filePath LIKE 'force-app/main/default/objects/%/webLinks/%.webLink-meta.xml' ORDER BY filePath")
|
|
5285
|
+
.all();
|
|
5286
|
+
const refreshed = [];
|
|
5287
|
+
for (const { filePath } of candidates) {
|
|
5288
|
+
if (/^force-app\/main\/default\/pages\/[A-Za-z]\w*\.page$/.test(filePath)) {
|
|
5289
|
+
if (!applicationNames.size)
|
|
5290
|
+
continue;
|
|
5291
|
+
const read = readWholeSource(repoPath, filePath);
|
|
5292
|
+
if (read.availability.state !== "available" ||
|
|
5293
|
+
!(await visualforceLightningBindings(read.text)).some((binding) => applicationNames.has(binding.name)))
|
|
5294
|
+
continue;
|
|
5295
|
+
await indexFile(path.join(repoPath, filePath), filePath, repoPath, db);
|
|
5296
|
+
recordIndexState(db, repoPath, filePath);
|
|
5297
|
+
refreshed.push(filePath);
|
|
5298
|
+
continue;
|
|
5299
|
+
}
|
|
5300
|
+
const metadata = salesforceMetadataFile(filePath);
|
|
5301
|
+
const auraSource = auraBundleFile(filePath);
|
|
5302
|
+
const lwcSource = lwcBundleFile(filePath);
|
|
5303
|
+
const format = metadata?.type === "flexipage" || metadata?.type === "flow" || metadata?.type === "weblink"
|
|
5304
|
+
? metadata.type
|
|
5305
|
+
: auraSource && (auraSource.extension === "cmp" || auraSource.extension === "app")
|
|
5306
|
+
? "aura"
|
|
5307
|
+
: lwcSource?.extension === "html"
|
|
5308
|
+
? "lwc"
|
|
5309
|
+
: undefined;
|
|
5310
|
+
// Filter by changed target category before opening source. An Apex-only
|
|
5311
|
+
// edit must not read every component template in a large repository.
|
|
5312
|
+
if (format
|
|
5313
|
+
? !(format === "weblink" ? pageNames : componentNames).size
|
|
5314
|
+
: metadata?.type === "quickAction"
|
|
5315
|
+
? !components.size
|
|
5316
|
+
: !classes.size)
|
|
5317
|
+
continue;
|
|
5318
|
+
const read = readWholeSource(repoPath, filePath);
|
|
5319
|
+
if (read.availability.state !== "available")
|
|
5320
|
+
continue;
|
|
5321
|
+
if (format) {
|
|
5322
|
+
const bindings = await salesforceComponentBindings(read.text, format);
|
|
5323
|
+
if (!bindings.some((binding) => (binding.target === "page"
|
|
5324
|
+
? pageNames
|
|
5325
|
+
: binding.target === "lwc-tag"
|
|
5326
|
+
? componentTags
|
|
5327
|
+
: componentNames).has(binding.name)))
|
|
5328
|
+
continue;
|
|
5329
|
+
if (metadata?.type === "flexipage" ||
|
|
5330
|
+
metadata?.type === "flow" ||
|
|
5331
|
+
metadata?.type === "weblink")
|
|
5332
|
+
await indexSalesforceMetadataFile(read.text, filePath, repoPath, db, metadata);
|
|
5333
|
+
else if (auraSource)
|
|
5334
|
+
await indexAuraBundleFile(read.text, filePath, repoPath, db, auraSource);
|
|
5335
|
+
else if (lwcSource)
|
|
5336
|
+
await indexLwcBundleFile(read.text, filePath, repoPath, db, lwcSource);
|
|
5337
|
+
}
|
|
5338
|
+
else if (metadata?.type === "quickAction") {
|
|
5339
|
+
const target = quickActionComponent(read.text);
|
|
5340
|
+
if (!target || !components.has(target.name))
|
|
5341
|
+
continue;
|
|
5342
|
+
await indexSalesforceMetadataFile(read.text, filePath, repoPath, db, metadata);
|
|
5343
|
+
}
|
|
5344
|
+
else {
|
|
5345
|
+
const aura = auraBundleFile(filePath);
|
|
5346
|
+
const lwc = lwcBundleFile(filePath);
|
|
5347
|
+
if (!aura && !lwc)
|
|
5348
|
+
continue;
|
|
5349
|
+
if (!appBuilderApexDataSources(read.text, !!aura).some((entry) => classes.has(entry.name)))
|
|
5350
|
+
continue;
|
|
5351
|
+
if (aura)
|
|
5352
|
+
await indexAuraBundleFile(read.text, filePath, repoPath, db, aura);
|
|
5353
|
+
else if (lwc)
|
|
5354
|
+
await indexLwcBundleFile(read.text, filePath, repoPath, db, lwc);
|
|
5355
|
+
}
|
|
5356
|
+
recordIndexState(db, repoPath, filePath);
|
|
5357
|
+
refreshed.push(filePath);
|
|
5358
|
+
}
|
|
5359
|
+
return refreshed;
|
|
5360
|
+
}
|
|
4227
5361
|
/**
|
|
4228
5362
|
* Revisit only indexed Flow files that literally name an incrementally changed
|
|
4229
5363
|
* Apex class. This keeps persisted method edges coherent when an annotation,
|
|
4230
5364
|
* overload, or declaration changes without requiring a repository-wide scan.
|
|
4231
5365
|
*/
|
|
4232
|
-
async function refreshFlowsForApexFiles(
|
|
5366
|
+
async function refreshFlowsForApexFiles(...args) {
|
|
5367
|
+
return extractionCoverage.run(args[0], () => refreshFlowsWithinCoverage(...args));
|
|
5368
|
+
}
|
|
5369
|
+
async function refreshFlowsWithinCoverage(db, repoPath, apexFiles) {
|
|
5370
|
+
const refreshed = await refreshSalesforceMetadataBindings(db, repoPath, apexFiles);
|
|
4233
5371
|
const classNames = new Set(apexFiles
|
|
4234
5372
|
.filter((file) => /^force-app\/main\/default\/classes\/[A-Za-z]\w*\.cls$/.test(file))
|
|
4235
5373
|
.map((file) => path.basename(file, ".cls")));
|
|
4236
5374
|
if (classNames.size === 0)
|
|
4237
|
-
return
|
|
5375
|
+
return refreshed;
|
|
4238
5376
|
const flowFiles = db
|
|
4239
5377
|
.query("SELECT DISTINCT filePath FROM symbols WHERE kind = 'salesforce_flow' AND filePath LIKE 'force-app/main/default/flows/%.flow-meta.xml' ORDER BY filePath")
|
|
4240
5378
|
.all();
|
|
4241
|
-
const refreshed = [];
|
|
4242
5379
|
for (const { filePath } of flowFiles) {
|
|
4243
5380
|
const absolute = path.join(repoPath, filePath);
|
|
4244
5381
|
let content;
|
|
@@ -4251,7 +5388,7 @@ async function refreshFlowsForApexFiles(db, repoPath, apexFiles) {
|
|
|
4251
5388
|
const mentionsChangedClass = [...content.matchAll(/<actionCalls>([\s\S]*?)<\/actionCalls>/g)]
|
|
4252
5389
|
.filter((match) => /<actionType>apex<\/actionType>/.test(match[1]))
|
|
4253
5390
|
.some((match) => staticXmlTagValues(match[1], "actionName").some((entry) => {
|
|
4254
|
-
const target = /^([A-Za-z]\w*)
|
|
5391
|
+
const target = /^([A-Za-z]\w*)(?:\.([A-Za-z]\w*))?$/.exec(entry.value);
|
|
4255
5392
|
return target ? classNames.has(target[1]) : false;
|
|
4256
5393
|
}));
|
|
4257
5394
|
if (!mentionsChangedClass)
|
|
@@ -4271,6 +5408,17 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
|
|
|
4271
5408
|
{ to: bundle.bundlePath, kind: "lwc_bundle_member" },
|
|
4272
5409
|
];
|
|
4273
5410
|
const references = [];
|
|
5411
|
+
if (bundle.extension === "js-meta.xml") {
|
|
5412
|
+
for (const entry of appBuilderApexDataSources(content, false)) {
|
|
5413
|
+
const target = findUniqueSalesforceBindingTarget(entry.name, repoPath, "apex");
|
|
5414
|
+
if (target)
|
|
5415
|
+
dependencies.push({
|
|
5416
|
+
to: target,
|
|
5417
|
+
kind: "appbuilder_apex_datasource",
|
|
5418
|
+
sourceEvidence: entry.evidence,
|
|
5419
|
+
});
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
4274
5422
|
if (bundle.extension === "js") {
|
|
4275
5423
|
const importedNames = new Map();
|
|
4276
5424
|
for (const match of content.matchAll(/import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']/g)) {
|
|
@@ -4338,6 +5486,7 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
|
|
|
4338
5486
|
}
|
|
4339
5487
|
}
|
|
4340
5488
|
if (bundle.extension === "html") {
|
|
5489
|
+
dependencies.push(...(await resolvedSalesforceComponentBindings(content, "lwc", repoPath)));
|
|
4341
5490
|
const markup = content.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
|
|
4342
5491
|
for (const match of markup.matchAll(/<(lightning-record-(?:form|view-form|edit-form|picker))\b/gi)) {
|
|
4343
5492
|
dependencies.push({
|
|
@@ -4372,13 +5521,13 @@ async function indexLwcBundleFile(content, relativePath, repoPath, db, bundle) {
|
|
|
4372
5521
|
}
|
|
4373
5522
|
insertRef.finalize();
|
|
4374
5523
|
const seen = new Set();
|
|
4375
|
-
const insertDep = db.prepare("INSERT INTO dependencies (fromFile, toFile, kind, confidence) VALUES (?, ?, ?, ?)");
|
|
5524
|
+
const insertDep = db.prepare("INSERT INTO dependencies (fromFile, toFile, kind, confidence, sourceEvidence) VALUES (?, ?, ?, ?, ?)");
|
|
4376
5525
|
for (const dependency of dependencies) {
|
|
4377
5526
|
const key = `${dependency.to}\0${dependency.kind}`;
|
|
4378
5527
|
if (seen.has(key))
|
|
4379
5528
|
continue;
|
|
4380
5529
|
seen.add(key);
|
|
4381
|
-
insertDep.run(relativePath, dependency.to, dependency.kind, 1);
|
|
5530
|
+
insertDep.run(relativePath, dependency.to, dependency.kind, 1, dependency.sourceEvidence ?? null);
|
|
4382
5531
|
}
|
|
4383
5532
|
insertDep.finalize();
|
|
4384
5533
|
db.run("COMMIT;");
|
|
@@ -5476,6 +6625,17 @@ async function indexAuraBundleFile(content, relativePath, repoPath, db, bundle)
|
|
|
5476
6625
|
sourceEvidence: "Aura bundle file path",
|
|
5477
6626
|
},
|
|
5478
6627
|
];
|
|
6628
|
+
if (bundle.extension === "design") {
|
|
6629
|
+
for (const entry of appBuilderApexDataSources(content, true)) {
|
|
6630
|
+
const target = findUniqueSalesforceBindingTarget(entry.name, repoPath, "apex");
|
|
6631
|
+
if (target)
|
|
6632
|
+
dependencies.push({
|
|
6633
|
+
to: target,
|
|
6634
|
+
kind: "appbuilder_apex_datasource",
|
|
6635
|
+
sourceEvidence: entry.evidence,
|
|
6636
|
+
});
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
5479
6639
|
// Only component controller attributes name an Apex controller. `$A` lookups,
|
|
5480
6640
|
// computed strings, and client-side action names deliberately produce no edge.
|
|
5481
6641
|
if (bundle.extension === "cmp") {
|
|
@@ -5491,17 +6651,9 @@ async function indexAuraBundleFile(content, relativePath, repoPath, db, bundle)
|
|
|
5491
6651
|
sourceEvidence: `controller="${controller}"`,
|
|
5492
6652
|
});
|
|
5493
6653
|
}
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
const target = findSalesforceLwcComponentFile(match[1], repoPath);
|
|
5498
|
-
if (target)
|
|
5499
|
-
dependencies.push({
|
|
5500
|
-
to: target,
|
|
5501
|
-
kind: "aura_lwc_component",
|
|
5502
|
-
sourceEvidence: match[0],
|
|
5503
|
-
});
|
|
5504
|
-
}
|
|
6654
|
+
}
|
|
6655
|
+
if (bundle.extension === "cmp" || bundle.extension === "app") {
|
|
6656
|
+
dependencies.push(...(await resolvedSalesforceComponentBindings(content, "aura", repoPath)));
|
|
5505
6657
|
}
|
|
5506
6658
|
db.run("BEGIN TRANSACTION;");
|
|
5507
6659
|
try {
|
|
@@ -5616,10 +6768,87 @@ async function indexSalesforceExperienceFile(content, relativePath, repoPath, db
|
|
|
5616
6768
|
throw error;
|
|
5617
6769
|
}
|
|
5618
6770
|
}
|
|
6771
|
+
/** Static Lightning Out declarations, never evidence that the call executed. */
|
|
6772
|
+
async function visualforceLightningBindings(content) {
|
|
6773
|
+
const scripts = visualforceInlineScripts(content);
|
|
6774
|
+
if (!scripts?.length)
|
|
6775
|
+
return [];
|
|
6776
|
+
const language = await getLanguageForFile("lightning-out.js");
|
|
6777
|
+
if (!language)
|
|
6778
|
+
return [];
|
|
6779
|
+
const parser = new Parser();
|
|
6780
|
+
parser.setLanguage(language);
|
|
6781
|
+
const bindings = [];
|
|
6782
|
+
try {
|
|
6783
|
+
for (const source of scripts) {
|
|
6784
|
+
const tree = parser.parse(source);
|
|
6785
|
+
try {
|
|
6786
|
+
if (tree.rootNode.hasError())
|
|
6787
|
+
return [];
|
|
6788
|
+
const queue = [tree.rootNode];
|
|
6789
|
+
for (let i = 0; i < queue.length; i++) {
|
|
6790
|
+
const node = queue[i];
|
|
6791
|
+
// References as well as direct calls can hide indirect eval or a
|
|
6792
|
+
// Function constructor; computed browser-global access can hide
|
|
6793
|
+
// a fragmented "$" + "Lightning" assignment. Fail closed.
|
|
6794
|
+
if ((["identifier", "property_identifier"].includes(node.type) &&
|
|
6795
|
+
["eval", "Function"].includes(node.text)) ||
|
|
6796
|
+
(node.type === "subscript_expression" &&
|
|
6797
|
+
["window", "globalThis", "self", "this"].includes(node.childForFieldName("object")?.text ?? "")))
|
|
6798
|
+
return [];
|
|
6799
|
+
if ((node.type === "identifier" || node.type === "shorthand_property_identifier_pattern") &&
|
|
6800
|
+
node.text === "$Lightning") {
|
|
6801
|
+
const parent = node.parent;
|
|
6802
|
+
if (parent?.type !== "member_expression" ||
|
|
6803
|
+
parent.childForFieldName("object")?.id !== node.id)
|
|
6804
|
+
return [];
|
|
6805
|
+
}
|
|
6806
|
+
if ((node.type === "property_identifier" && node.text === "$Lightning") ||
|
|
6807
|
+
(node.type === "subscript_expression" && /\$Lightning/.test(node.text)))
|
|
6808
|
+
return [];
|
|
6809
|
+
if ([
|
|
6810
|
+
"assignment_expression",
|
|
6811
|
+
"augmented_assignment_expression",
|
|
6812
|
+
"update_expression",
|
|
6813
|
+
"unary_expression",
|
|
6814
|
+
].includes(node.type) &&
|
|
6815
|
+
/\$Lightning/.test((node.childForFieldName("left") ?? node.childForFieldName("argument") ?? node).text))
|
|
6816
|
+
return [];
|
|
6817
|
+
if (node.type === "with_statement" ||
|
|
6818
|
+
(node.type === "call_expression" &&
|
|
6819
|
+
["eval", "Function"].includes(node.childForFieldName("function")?.text ?? "")))
|
|
6820
|
+
return [];
|
|
6821
|
+
if (node.type === "call_expression") {
|
|
6822
|
+
const call = node.childForFieldName("function");
|
|
6823
|
+
const argument = node.childForFieldName("arguments")?.namedChild(0);
|
|
6824
|
+
if (call?.type === "member_expression" &&
|
|
6825
|
+
call.text === "$Lightning.use" &&
|
|
6826
|
+
argument?.type === "string") {
|
|
6827
|
+
const name = /^["']c:([A-Za-z]\w*)["']$/.exec(argument.text)?.[1];
|
|
6828
|
+
if (name && !name.includes("__"))
|
|
6829
|
+
bindings.push({ name, evidence: `${call.text}(${argument.text}` });
|
|
6830
|
+
}
|
|
6831
|
+
}
|
|
6832
|
+
queue.push(...node.namedChildren);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
finally {
|
|
6836
|
+
tree.delete();
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
return [...new Map(bindings.map((binding) => [binding.name, binding])).values()];
|
|
6840
|
+
}
|
|
6841
|
+
finally {
|
|
6842
|
+
parser.delete();
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
5619
6845
|
async function indexVisualforceFile(content, tree, relativePath, repoPath, db) {
|
|
5620
6846
|
const controllerNames = new Set();
|
|
5621
6847
|
const extensionNames = [];
|
|
5622
6848
|
const references = [];
|
|
6849
|
+
const lightning = /^force-app\/main\/default\/pages\/[A-Za-z]\w*\.page$/.test(relativePath)
|
|
6850
|
+
? await visualforceLightningBindings(content)
|
|
6851
|
+
: [];
|
|
5623
6852
|
function traverseXML(node) {
|
|
5624
6853
|
if (node.type === "element" || node.type === "start_tag") {
|
|
5625
6854
|
const tagNameNode = node.childForFieldName("name") || node.children.find((c) => c.type === "tag_name");
|
|
@@ -5747,6 +6976,14 @@ async function indexVisualforceFile(content, tree, relativePath, repoPath, db) {
|
|
|
5747
6976
|
const target = resolvedPath || className;
|
|
5748
6977
|
insertDep.run(relativePath, target, "controller", 1, `${attribute}="${className}"`);
|
|
5749
6978
|
}
|
|
6979
|
+
for (const binding of lightning) {
|
|
6980
|
+
const target = salesforceBindingTargets(repoPath).components.resolve({
|
|
6981
|
+
target: "application",
|
|
6982
|
+
name: binding.name,
|
|
6983
|
+
});
|
|
6984
|
+
if (target)
|
|
6985
|
+
insertDep.run(relativePath, target.file, "visualforce_lightning_out", 1, binding.evidence);
|
|
6986
|
+
}
|
|
5750
6987
|
insertDep.finalize();
|
|
5751
6988
|
db.run("COMMIT;");
|
|
5752
6989
|
}
|
|
@@ -6260,9 +7497,10 @@ function buildFileIndexResult(tree, content, isPython, isApex, absolutePath, rel
|
|
|
6260
7497
|
return null;
|
|
6261
7498
|
};
|
|
6262
7499
|
const resolvedReferences = references.map((ref) => ({
|
|
7500
|
+
apexCall: ref.apexCall,
|
|
6263
7501
|
callerSymbol: ref.callerSymbol,
|
|
6264
7502
|
calleeSymbol: ref.calleeSymbol,
|
|
6265
|
-
calleeFile: resolveCalleeFile(ref.calleeSymbol, ref.calleeModule),
|
|
7503
|
+
calleeFile: ref.apexCall ? null : resolveCalleeFile(ref.calleeSymbol, ref.calleeModule),
|
|
6266
7504
|
line: ref.line,
|
|
6267
7505
|
column: ref.column,
|
|
6268
7506
|
kind: ref.kind ?? "call",
|
|
@@ -6293,6 +7531,10 @@ function buildFileIndexResult(tree, content, isPython, isApex, absolutePath, rel
|
|
|
6293
7531
|
return {
|
|
6294
7532
|
relativePath,
|
|
6295
7533
|
mcpRegistrations,
|
|
7534
|
+
...(isApex
|
|
7535
|
+
? { apexClassUses: extractApexClassUses(tree.rootNode, content, relativePath) }
|
|
7536
|
+
: {}),
|
|
7537
|
+
...(isApex ? { apexReceiverVersion: 1 } : {}),
|
|
6296
7538
|
definitions,
|
|
6297
7539
|
references: resolvedReferences,
|
|
6298
7540
|
apiContracts,
|
|
@@ -6323,6 +7565,26 @@ function writeFileIndexResult(db, result) {
|
|
|
6323
7565
|
/** The actual delete+insert statements, run inside a transaction/savepoint the caller owns. */
|
|
6324
7566
|
function writeFileIndexResultUnwrapped(db, result) {
|
|
6325
7567
|
const { relativePath, mcpRegistrations, definitions, references, apiContracts, dependencies } = result;
|
|
7568
|
+
db.run("DELETE FROM apex_consumer_files WHERE filePath = ?", [relativePath]);
|
|
7569
|
+
db.run("DELETE FROM apex_class_uses WHERE filePath = ?", [relativePath]);
|
|
7570
|
+
if (result.apexClassUses) {
|
|
7571
|
+
const facts = result.apexClassUses;
|
|
7572
|
+
const serialized = JSON.stringify(facts);
|
|
7573
|
+
db.run("INSERT INTO apex_consumer_files(filePath,version,sourceHash,parseState,isTest,facts,factsHash) VALUES (?,1,?,?,?,?,?)", [
|
|
7574
|
+
relativePath,
|
|
7575
|
+
facts.sourceHash,
|
|
7576
|
+
facts.parseState,
|
|
7577
|
+
facts.isTest === null ? null : facts.isTest ? 1 : 0,
|
|
7578
|
+
serialized,
|
|
7579
|
+
crypto.createHash("sha256").update(serialized).digest("hex"),
|
|
7580
|
+
]);
|
|
7581
|
+
setMeta(db, "apexConsumerFactsRevision", crypto.randomUUID());
|
|
7582
|
+
}
|
|
7583
|
+
db.run("DELETE FROM apex_file_capabilities WHERE filePath = ?", [relativePath]);
|
|
7584
|
+
if (result.apexReceiverVersion === 1)
|
|
7585
|
+
db.run("INSERT OR REPLACE INTO apex_file_capabilities(filePath,version) VALUES (?,1)", [
|
|
7586
|
+
relativePath,
|
|
7587
|
+
]);
|
|
6326
7588
|
db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
|
|
6327
7589
|
deleteSymbolsForFile(db, relativePath);
|
|
6328
7590
|
db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
|
|
@@ -6337,19 +7599,19 @@ function writeFileIndexResultUnwrapped(db, result) {
|
|
|
6337
7599
|
}
|
|
6338
7600
|
insertMcp.finalize();
|
|
6339
7601
|
const insertSym = db.prepare(`
|
|
6340
|
-
INSERT INTO symbols (name, kind, filePath, startLine, endLine, startCol, endCol, summary)
|
|
6341
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
7602
|
+
INSERT INTO symbols (name, kind, filePath, startLine, endLine, startCol, endCol, summary, apexDeclaration)
|
|
7603
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6342
7604
|
`);
|
|
6343
7605
|
for (const def of definitions) {
|
|
6344
|
-
insertSym.run(def.name, def.kind, relativePath, def.startLine, def.endLine, def.startCol, def.endCol, def.summary);
|
|
7606
|
+
insertSym.run(def.name, def.kind, relativePath, def.startLine, def.endLine, def.startCol, def.endCol, def.summary, def.apexDeclaration ?? null);
|
|
6345
7607
|
}
|
|
6346
7608
|
insertSym.finalize();
|
|
6347
7609
|
const insertRef = db.prepare(`
|
|
6348
|
-
INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind)
|
|
6349
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
7610
|
+
INSERT INTO "references" (callerSymbol, callerFile, calleeSymbol, calleeFile, line, column, kind, apexCall)
|
|
7611
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6350
7612
|
`);
|
|
6351
7613
|
for (const ref of references) {
|
|
6352
|
-
insertRef.run(ref.callerSymbol, relativePath, ref.calleeSymbol, ref.calleeFile, ref.line, ref.column, ref.kind);
|
|
7614
|
+
insertRef.run(ref.callerSymbol, relativePath, ref.calleeSymbol, ref.calleeFile, ref.line, ref.column, ref.kind, ref.apexCall ?? null);
|
|
6353
7615
|
}
|
|
6354
7616
|
insertRef.finalize();
|
|
6355
7617
|
const insertApiContract = db.prepare(`
|
|
@@ -6470,6 +7732,34 @@ function freeQuietly(target) {
|
|
|
6470
7732
|
// Intentionally ignored; see above.
|
|
6471
7733
|
}
|
|
6472
7734
|
}
|
|
7735
|
+
function incompleteApexExtraction(content, relativePath, reason) {
|
|
7736
|
+
const facts = {
|
|
7737
|
+
version: 1,
|
|
7738
|
+
file: relativePath,
|
|
7739
|
+
sourceHash: crypto.createHash("sha256").update(content).digest("hex"),
|
|
7740
|
+
parseState: "partial",
|
|
7741
|
+
isTest: null,
|
|
7742
|
+
sites: [],
|
|
7743
|
+
declarations: [],
|
|
7744
|
+
unsupportedUses: [],
|
|
7745
|
+
coverage: "supported-syntax-only",
|
|
7746
|
+
omissionReason: reason,
|
|
7747
|
+
};
|
|
7748
|
+
return {
|
|
7749
|
+
kind: "result",
|
|
7750
|
+
result: {
|
|
7751
|
+
relativePath,
|
|
7752
|
+
apexClassUses: facts,
|
|
7753
|
+
apexClassUseIncompleteReason: reason,
|
|
7754
|
+
mcpRegistrations: [],
|
|
7755
|
+
definitions: [],
|
|
7756
|
+
references: [],
|
|
7757
|
+
apiContracts: [],
|
|
7758
|
+
dependencies: [],
|
|
7759
|
+
apexPlatformDependencies: [],
|
|
7760
|
+
},
|
|
7761
|
+
};
|
|
7762
|
+
}
|
|
6473
7763
|
/**
|
|
6474
7764
|
* Read, parse, and extract one file through the generic tree-sitter path.
|
|
6475
7765
|
*
|
|
@@ -6479,15 +7769,23 @@ function freeQuietly(target) {
|
|
|
6479
7769
|
* through here so routing can never drift between them.
|
|
6480
7770
|
*/
|
|
6481
7771
|
export async function extractGenericFileForIndex(absolutePath, relativePath, repoPath) {
|
|
7772
|
+
const isApex = absolutePath.endsWith(".cls") || absolutePath.endsWith(".trigger");
|
|
6482
7773
|
const lang = await measurePerfPhase("parser_wasm_init", () => getLanguageForFile(absolutePath));
|
|
6483
7774
|
if (!lang)
|
|
6484
|
-
return
|
|
7775
|
+
return isApex
|
|
7776
|
+
? incompleteApexExtraction(fs.readFileSync(absolutePath, "utf8"), relativePath, "parser-unavailable")
|
|
7777
|
+
: { kind: "unparsed" };
|
|
6485
7778
|
const content = measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
|
|
6486
7779
|
// Skip minified / generated bundles (e.g. a `*.bundle.js`): they're not
|
|
6487
7780
|
// readable source and would inject thousands of junk "symbols" that pollute
|
|
6488
7781
|
// the graph, communities, and hubs. Heuristic: a very high average line
|
|
6489
7782
|
// length (minified code packs everything onto a few enormous lines).
|
|
6490
|
-
|
|
7783
|
+
// Apex response fixtures legitimately contain huge single-line string literals. Line
|
|
7784
|
+
// density is not a sound minification signal for that language; preserve the existing
|
|
7785
|
+
// absolute source-length bound and record its omission rather than inventing empty coverage.
|
|
7786
|
+
if (isApex && content.length > 1_000_000)
|
|
7787
|
+
return incompleteApexExtraction(content, relativePath, "source-size-limit");
|
|
7788
|
+
if (!isApex && isMinifiedSource(content))
|
|
6491
7789
|
return { kind: "unparsed" };
|
|
6492
7790
|
const isSql = absolutePath.endsWith(".sql") || absolutePath.endsWith(".pkb") || absolutePath.endsWith(".pks");
|
|
6493
7791
|
// SQL: neutralize dollar-quoted bodies that crash the grammar's scanner.
|
|
@@ -6514,10 +7812,14 @@ export async function extractGenericFileForIndex(absolutePath, relativePath, rep
|
|
|
6514
7812
|
// to an abort through the recovery path itself.
|
|
6515
7813
|
freeQuietly(parser);
|
|
6516
7814
|
console.warn(`knodin: skipped ${relativePath} (parser error: ${e.message})`);
|
|
7815
|
+
if (isApex)
|
|
7816
|
+
return incompleteApexExtraction(content, relativePath, "parser-failure");
|
|
6517
7817
|
return { kind: "skipped", reason: e.message };
|
|
6518
7818
|
}
|
|
6519
7819
|
if (!tree) {
|
|
6520
7820
|
freeQuietly(parser);
|
|
7821
|
+
if (isApex)
|
|
7822
|
+
return incompleteApexExtraction(content, relativePath, "parser-failure");
|
|
6521
7823
|
return { kind: "skipped", reason: "parser returned no tree" };
|
|
6522
7824
|
}
|
|
6523
7825
|
// Everything from here on can throw — `extractMcpToolRegistrations` and the
|
|
@@ -6531,7 +7833,6 @@ export async function extractGenericFileForIndex(absolutePath, relativePath, rep
|
|
|
6531
7833
|
? extractMcpToolRegistrations(content, relativePath, tree.rootNode)
|
|
6532
7834
|
: [];
|
|
6533
7835
|
const isPython = absolutePath.endsWith(".py");
|
|
6534
|
-
const isApex = absolutePath.endsWith(".cls") || absolutePath.endsWith(".trigger");
|
|
6535
7836
|
const isVisualforce = absolutePath.endsWith(".page") || absolutePath.endsWith(".component");
|
|
6536
7837
|
const isPrisma = absolutePath.endsWith(".prisma");
|
|
6537
7838
|
const isWorkday = absolutePath.endsWith(".clp") ||
|
|
@@ -6670,7 +7971,14 @@ const SPECIAL_ROUTE_EXTENSIONS = [
|
|
|
6670
7971
|
".pks",
|
|
6671
7972
|
".prisma",
|
|
6672
7973
|
];
|
|
6673
|
-
|
|
7974
|
+
const extractionCoverage = new AsyncLocalStorage();
|
|
7975
|
+
async function indexFile(...args) {
|
|
7976
|
+
const [, , , db] = args;
|
|
7977
|
+
if (!coverageContains(requireIndexCoverage(coverageForDatabase(db)), args[1]))
|
|
7978
|
+
throw new Error(`knodin: refusing to index outside declared coverage: ${args[1]}`);
|
|
7979
|
+
return extractionCoverage.run(db, () => indexFileInCoverage(...args));
|
|
7980
|
+
}
|
|
7981
|
+
async function indexFileInCoverage(absolutePath, relativePath, repoPath, db, unparsed, collectResult) {
|
|
6674
7982
|
try {
|
|
6675
7983
|
invalidateScipFacts(db);
|
|
6676
7984
|
const read = () => measurePerfPhaseSync("read", () => fs.readFileSync(absolutePath, "utf-8"));
|
|
@@ -6765,6 +8073,8 @@ async function indexFile(absolutePath, relativePath, repoPath, db, unparsed, col
|
|
|
6765
8073
|
}
|
|
6766
8074
|
return;
|
|
6767
8075
|
}
|
|
8076
|
+
if (extraction.result.apexClassUseIncompleteReason && unparsed)
|
|
8077
|
+
tallyOne(unparsed, extensionBucket(relativePath));
|
|
6768
8078
|
if (collectResult) {
|
|
6769
8079
|
collectResult(extraction.result);
|
|
6770
8080
|
}
|
|
@@ -6792,7 +8102,7 @@ async function indexFile(absolutePath, relativePath, repoPath, db, unparsed, col
|
|
|
6792
8102
|
* stale wiring atomically without a second schema or cache.
|
|
6793
8103
|
*/
|
|
6794
8104
|
function reconcileTypeScriptDi(db, repoPath) {
|
|
6795
|
-
const files = collectRepoFiles(repoPath).filter((file) => (file.endsWith(".ts") || file.endsWith(".tsx")) && !file.endsWith(".d.ts"));
|
|
8105
|
+
const files = collectRepoFiles(repoPath, db).filter((file) => (file.endsWith(".ts") || file.endsWith(".tsx")) && !file.endsWith(".d.ts"));
|
|
6796
8106
|
const frameworkFiles = files.filter((file) => {
|
|
6797
8107
|
try {
|
|
6798
8108
|
const source = fs.readFileSync(path.join(repoPath, file), "utf8");
|
|
@@ -7381,10 +8691,10 @@ async function indexEmbeddings(db, repoPath, progress, signal) {
|
|
|
7381
8691
|
progress?.("embedding-symbols", 0, `Embedding ${missing.length.toLocaleString()} symbols`, {
|
|
7382
8692
|
phaseTotal: missing.length,
|
|
7383
8693
|
});
|
|
7384
|
-
progress?.("embedding-model", 0, "Preparing local embedding model");
|
|
7385
8694
|
let completedEmbeddings = 0;
|
|
8695
|
+
let modelProgressComplete = false;
|
|
7386
8696
|
const onModelProgress = (event) => {
|
|
7387
|
-
if (
|
|
8697
|
+
if (modelProgressComplete)
|
|
7388
8698
|
return;
|
|
7389
8699
|
const completed = event.loaded ?? 0;
|
|
7390
8700
|
const total = event.total;
|
|
@@ -7404,6 +8714,8 @@ async function indexEmbeddings(db, repoPath, progress, signal) {
|
|
|
7404
8714
|
`);
|
|
7405
8715
|
const batchSize = embeddingBatchSize();
|
|
7406
8716
|
const sourceForEmbedding = createEmbeddingSourceReader(repoPath);
|
|
8717
|
+
const reuse = new EmbeddingReuseCache(db);
|
|
8718
|
+
const modelIdentity = getEmbeddingCacheIdentity();
|
|
7407
8719
|
let lastYieldAt = Date.now();
|
|
7408
8720
|
for (let offset = 0; offset < missing.length; offset += batchSize) {
|
|
7409
8721
|
throwIfAborted(signal);
|
|
@@ -7411,14 +8723,33 @@ async function indexEmbeddings(db, repoPath, progress, signal) {
|
|
|
7411
8723
|
const source = sourceForEmbedding(sym.filePath, sym.startLine, sym.endLine);
|
|
7412
8724
|
return { symbol: sym, text: embeddingInputFor(sym, source) };
|
|
7413
8725
|
});
|
|
7414
|
-
const
|
|
8726
|
+
const cached = [];
|
|
8727
|
+
const uncached = [];
|
|
8728
|
+
const keys = new Map();
|
|
8729
|
+
for (const item of prepared) {
|
|
8730
|
+
const key = embeddingReuseKey(modelIdentity, item.text);
|
|
8731
|
+
keys.set(item.symbol.id, key);
|
|
8732
|
+
const embedding = reuse.get(key);
|
|
8733
|
+
if (embedding)
|
|
8734
|
+
cached.push({ symbol: item.symbol, embedding });
|
|
8735
|
+
else
|
|
8736
|
+
uncached.push(item);
|
|
8737
|
+
}
|
|
8738
|
+
const generated = [
|
|
8739
|
+
...cached,
|
|
8740
|
+
...(uncached.length ? await embedPreparedBatch(uncached, onModelProgress) : []),
|
|
8741
|
+
];
|
|
8742
|
+
if (uncached.length > 0)
|
|
8743
|
+
modelProgressComplete = true;
|
|
7415
8744
|
throwIfAborted(signal);
|
|
7416
8745
|
db.run("BEGIN TRANSACTION;");
|
|
7417
8746
|
try {
|
|
7418
8747
|
for (const { symbol, embedding: embeddingVec } of generated) {
|
|
7419
8748
|
const u8 = new Uint8Array(embeddingVec.buffer, embeddingVec.byteOffset, embeddingVec.byteLength);
|
|
7420
8749
|
insertEmbedding.run(symbol.id, u8);
|
|
8750
|
+
reuse.put(keys.get(symbol.id), embeddingVec);
|
|
7421
8751
|
}
|
|
8752
|
+
reuse.prune();
|
|
7422
8753
|
db.run("COMMIT;");
|
|
7423
8754
|
}
|
|
7424
8755
|
catch (error) {
|
|
@@ -7460,8 +8791,8 @@ function extensionBucket(relativePath) {
|
|
|
7460
8791
|
function tallyOne(tally, bucket) {
|
|
7461
8792
|
tally.set(bucket, (tally.get(bucket) ?? 0) + 1);
|
|
7462
8793
|
}
|
|
7463
|
-
function collectRepoFiles(repoPath) {
|
|
7464
|
-
return collectRepoFilesWithCoverage(repoPath).files;
|
|
8794
|
+
function collectRepoFiles(repoPath, db) {
|
|
8795
|
+
return collectRepoFilesWithCoverage(repoPath, db).files;
|
|
7465
8796
|
}
|
|
7466
8797
|
/** Descending-count record, so the largest coverage gap reads first. */
|
|
7467
8798
|
function sortedTally(tally) {
|
|
@@ -7664,7 +8995,8 @@ function isIndexableJsonCandidate(file, repoPath) {
|
|
|
7664
8995
|
return false;
|
|
7665
8996
|
}
|
|
7666
8997
|
}
|
|
7667
|
-
function gitTrackedCandidates(repoPath) {
|
|
8998
|
+
function gitTrackedCandidates(repoPath, intent) {
|
|
8999
|
+
let gitConfirmed = false;
|
|
7668
9000
|
try {
|
|
7669
9001
|
const gitRoot = child_process
|
|
7670
9002
|
.execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -7675,21 +9007,31 @@ function gitTrackedCandidates(repoPath) {
|
|
|
7675
9007
|
.trim();
|
|
7676
9008
|
if (!gitRoot || !isSameDir(gitRoot, repoPath))
|
|
7677
9009
|
return null;
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
9010
|
+
gitConfirmed = true;
|
|
9011
|
+
let candidates = [];
|
|
9012
|
+
for (const pathspecs of coverageGitPathspecBatches(intent))
|
|
9013
|
+
candidates = candidates.concat(splitNulPaths(child_process.execFileSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard", ...pathspecs], {
|
|
9014
|
+
cwd: repoPath,
|
|
9015
|
+
encoding: "utf8",
|
|
9016
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9017
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
9018
|
+
})));
|
|
9019
|
+
return (intent?.mode === "scoped" ? [...new Set(candidates)] : candidates).filter((file) => {
|
|
9020
|
+
if (intent && !coverageContains(intent, file))
|
|
9021
|
+
return false;
|
|
7684
9022
|
try {
|
|
7685
|
-
return
|
|
9023
|
+
return intent?.mode === "scoped"
|
|
9024
|
+
? (safeCoveragePath(repoPath, file)?.isFile() ?? false)
|
|
9025
|
+
: fs.statSync(path.join(repoPath, file)).isFile();
|
|
7686
9026
|
}
|
|
7687
9027
|
catch {
|
|
7688
9028
|
return false;
|
|
7689
9029
|
}
|
|
7690
9030
|
});
|
|
7691
9031
|
}
|
|
7692
|
-
catch {
|
|
9032
|
+
catch (error) {
|
|
9033
|
+
if (intent?.mode === "scoped" && (gitConfirmed || fs.existsSync(path.join(repoPath, ".git"))))
|
|
9034
|
+
throw new Error("knodin: scoped Git file enumeration failed; refusing an unfiltered filesystem fallback", { cause: error });
|
|
7693
9035
|
return null;
|
|
7694
9036
|
}
|
|
7695
9037
|
}
|
|
@@ -7708,29 +9050,41 @@ async function execGitAbortable(repoPath, args, signal) {
|
|
|
7708
9050
|
: resolve(stdout));
|
|
7709
9051
|
});
|
|
7710
9052
|
}
|
|
7711
|
-
async function collectRepoFilesWithCoverageAbortable(repoPath, signal) {
|
|
9053
|
+
async function collectRepoFilesWithCoverageAbortable(repoPath, signal, db) {
|
|
9054
|
+
const intent = requireIndexCoverage(coverageForDatabase(db ?? dbInstances.get(path.resolve(repoPath))));
|
|
7712
9055
|
let candidates = null;
|
|
9056
|
+
let gitConfirmed = false;
|
|
7713
9057
|
try {
|
|
7714
9058
|
const gitRoot = (await execGitAbortable(repoPath, ["rev-parse", "--show-toplevel"], signal)).trim();
|
|
7715
9059
|
if (gitRoot && isSameDir(gitRoot, repoPath)) {
|
|
7716
|
-
|
|
9060
|
+
gitConfirmed = true;
|
|
9061
|
+
let selected = [];
|
|
9062
|
+
for (const pathspecs of coverageGitPathspecBatches(intent))
|
|
9063
|
+
selected = selected.concat(splitNulPaths(await execGitAbortable(repoPath, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", ...pathspecs], signal)));
|
|
9064
|
+
candidates = intent.mode === "scoped" ? [...new Set(selected)] : selected;
|
|
7717
9065
|
}
|
|
7718
9066
|
}
|
|
7719
|
-
catch {
|
|
9067
|
+
catch (error) {
|
|
7720
9068
|
throwIfAborted(signal);
|
|
9069
|
+
if (intent.mode === "scoped" && (gitConfirmed || fs.existsSync(path.join(repoPath, ".git"))))
|
|
9070
|
+
throw new Error("knodin: scoped Git file enumeration failed; refusing an unfiltered filesystem fallback", { cause: error });
|
|
7721
9071
|
}
|
|
7722
9072
|
const files = [];
|
|
7723
9073
|
const skippedByExtension = new Map();
|
|
7724
|
-
const source = candidates ??
|
|
9074
|
+
const source = candidates ?? walkCoverageFiles(repoPath, intent);
|
|
7725
9075
|
for (let index = 0; index < source.length; index++) {
|
|
7726
9076
|
if (index % 256 === 0) {
|
|
7727
9077
|
throwIfAborted(signal);
|
|
7728
9078
|
await yieldToIndexEventLoop();
|
|
7729
9079
|
}
|
|
7730
9080
|
const file = source[index];
|
|
9081
|
+
if (!coverageContains(intent, file))
|
|
9082
|
+
continue;
|
|
7731
9083
|
if (candidates) {
|
|
7732
9084
|
try {
|
|
7733
|
-
if (!
|
|
9085
|
+
if (!(intent.mode === "scoped"
|
|
9086
|
+
? safeCoveragePath(repoPath, file)?.isFile()
|
|
9087
|
+
: fs.statSync(path.join(repoPath, file)).isFile()))
|
|
7734
9088
|
continue;
|
|
7735
9089
|
}
|
|
7736
9090
|
catch {
|
|
@@ -7755,17 +9109,21 @@ async function collectRepoFilesWithCoverageAbortable(repoPath, signal) {
|
|
|
7755
9109
|
* `manifest.json` files and Dockerfiles. Shared by the full index and the
|
|
7756
9110
|
* cold-start reconcile so both see the identical candidate set.
|
|
7757
9111
|
*/
|
|
7758
|
-
function collectRepoFilesWithCoverage(repoPath) {
|
|
9112
|
+
function collectRepoFilesWithCoverage(repoPath, db, requestedIntent) {
|
|
9113
|
+
const intent = requestedIntent ??
|
|
9114
|
+
requireIndexCoverage(coverageForDatabase(db ?? dbInstances.get(path.resolve(repoPath))));
|
|
7759
9115
|
const files = [];
|
|
7760
9116
|
const skippedByExtension = new Map();
|
|
7761
|
-
const candidates = gitTrackedCandidates(repoPath);
|
|
9117
|
+
const candidates = gitTrackedCandidates(repoPath, intent);
|
|
7762
9118
|
// The walk deliberately does NOT pre-filter with `accept`: both branches must
|
|
7763
9119
|
// see the same candidate superset so the skip tally below counts identically
|
|
7764
9120
|
// whether or not the repository is a Git work tree. (main passed
|
|
7765
9121
|
// `{ accept: isIndexableSourcePath }` here; filtering during the walk would
|
|
7766
9122
|
// make the excluded files invisible to the tally, which is the whole point of
|
|
7767
9123
|
// this function.)
|
|
7768
|
-
for (const file of candidates ??
|
|
9124
|
+
for (const file of candidates ?? walkCoverageFiles(repoPath, intent)) {
|
|
9125
|
+
if (!coverageContains(intent, file))
|
|
9126
|
+
continue;
|
|
7769
9127
|
// Pruned infrastructure (node_modules, dist, .git, state dirs) is not a
|
|
7770
9128
|
// coverage gap and is never counted as one.
|
|
7771
9129
|
if (!isIndexablePath(file))
|
|
@@ -7792,7 +9150,7 @@ function collectRepoFilesWithCoverage(repoPath) {
|
|
|
7792
9150
|
function gitHead(repoPath) {
|
|
7793
9151
|
try {
|
|
7794
9152
|
const out = child_process
|
|
7795
|
-
.
|
|
9153
|
+
.execFileSync("git", ["rev-parse", "HEAD"], {
|
|
7796
9154
|
cwd: repoPath,
|
|
7797
9155
|
encoding: "utf-8",
|
|
7798
9156
|
stdio: ["pipe", "pipe", "ignore"],
|
|
@@ -7800,28 +9158,119 @@ function gitHead(repoPath) {
|
|
|
7800
9158
|
.trim();
|
|
7801
9159
|
return out || null;
|
|
7802
9160
|
}
|
|
7803
|
-
catch {
|
|
7804
|
-
return null;
|
|
9161
|
+
catch {
|
|
9162
|
+
return null;
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9165
|
+
/** Read a value from the `meta` key/value table (null if absent). */
|
|
9166
|
+
function getMeta(db, key) {
|
|
9167
|
+
try {
|
|
9168
|
+
const row = db
|
|
9169
|
+
.query("SELECT value FROM meta WHERE key = ?")
|
|
9170
|
+
.get(key);
|
|
9171
|
+
return row?.value ?? null;
|
|
9172
|
+
}
|
|
9173
|
+
catch {
|
|
9174
|
+
return null;
|
|
9175
|
+
}
|
|
9176
|
+
}
|
|
9177
|
+
/** Upsert a value into the `meta` key/value table. */
|
|
9178
|
+
function setMeta(db, key, value) {
|
|
9179
|
+
try {
|
|
9180
|
+
db.run("INSERT INTO meta(key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value]);
|
|
9181
|
+
}
|
|
9182
|
+
catch (_) { }
|
|
9183
|
+
}
|
|
9184
|
+
const coverageStates = new WeakMap();
|
|
9185
|
+
function coverageForDatabase(db) {
|
|
9186
|
+
if (!db)
|
|
9187
|
+
return readIndexCoverage(null, null, null);
|
|
9188
|
+
const values = [
|
|
9189
|
+
getMeta(db, INDEX_COVERAGE_KEY),
|
|
9190
|
+
getMeta(db, INDEX_COVERAGE_PENDING_KEY),
|
|
9191
|
+
getMeta(db, INDEX_COVERAGE_COMPLETE_KEY),
|
|
9192
|
+
];
|
|
9193
|
+
const key = JSON.stringify(values);
|
|
9194
|
+
const cached = coverageStates.get(db);
|
|
9195
|
+
if (cached?.key === key)
|
|
9196
|
+
return cached.state;
|
|
9197
|
+
const state = readIndexCoverage(...values);
|
|
9198
|
+
coverageStates.set(db, { key, state });
|
|
9199
|
+
return state;
|
|
9200
|
+
}
|
|
9201
|
+
function writeCoverageIntent(db, repoPath, intent) {
|
|
9202
|
+
// Unlike best-effort informational meta, scope intent must commit or fail BEFORE work.
|
|
9203
|
+
db.run("BEGIN TRANSACTION;");
|
|
9204
|
+
try {
|
|
9205
|
+
for (const [key, value] of [
|
|
9206
|
+
[INDEX_COVERAGE_KEY, JSON.stringify(intent)],
|
|
9207
|
+
[INDEX_COVERAGE_PENDING_KEY, String(intent.revision)],
|
|
9208
|
+
])
|
|
9209
|
+
db.run("INSERT INTO meta(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
|
|
9210
|
+
db.run("COMMIT;");
|
|
9211
|
+
}
|
|
9212
|
+
catch (error) {
|
|
9213
|
+
db.run("ROLLBACK;");
|
|
9214
|
+
throw error;
|
|
9215
|
+
}
|
|
9216
|
+
freshnessProbes.delete(path.resolve(repoPath));
|
|
9217
|
+
statusCache.delete(path.resolve(repoPath));
|
|
9218
|
+
invalidateResolutionCaches(repoPath);
|
|
9219
|
+
indexGeneration++;
|
|
9220
|
+
}
|
|
9221
|
+
function markCoverageIntentComplete(db) {
|
|
9222
|
+
const state = coverageForDatabase(db);
|
|
9223
|
+
if (state.origin !== "explicit" || !state.intent)
|
|
9224
|
+
return;
|
|
9225
|
+
db.run("INSERT INTO meta(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [INDEX_COVERAGE_COMPLETE_KEY, String(state.intent.revision)]);
|
|
9226
|
+
db.run("DELETE FROM meta WHERE key = ?", [INDEX_COVERAGE_PENDING_KEY]);
|
|
9227
|
+
}
|
|
9228
|
+
function completeCoverageIntent(db) {
|
|
9229
|
+
db.run("BEGIN TRANSACTION;");
|
|
9230
|
+
try {
|
|
9231
|
+
markCoverageIntentComplete(db);
|
|
9232
|
+
db.run("COMMIT;");
|
|
9233
|
+
}
|
|
9234
|
+
catch (error) {
|
|
9235
|
+
db.run("ROLLBACK;");
|
|
9236
|
+
throw error;
|
|
7805
9237
|
}
|
|
7806
9238
|
}
|
|
7807
|
-
/**
|
|
7808
|
-
function
|
|
9239
|
+
/** No selector can follow a symlink, including a symlink in a parent directory. */
|
|
9240
|
+
function safeCoveragePath(repoPath, file) {
|
|
9241
|
+
if (!canonicalCoveragePath(file) || !isIndexablePath(file))
|
|
9242
|
+
return null;
|
|
9243
|
+
let absolute = repoPath;
|
|
9244
|
+
let stat = null;
|
|
7809
9245
|
try {
|
|
7810
|
-
const
|
|
7811
|
-
.
|
|
7812
|
-
.
|
|
7813
|
-
|
|
9246
|
+
for (const part of file.split("/")) {
|
|
9247
|
+
absolute = path.join(absolute, part);
|
|
9248
|
+
stat = fs.lstatSync(absolute);
|
|
9249
|
+
if (stat.isSymbolicLink())
|
|
9250
|
+
return null;
|
|
9251
|
+
}
|
|
9252
|
+
return stat;
|
|
7814
9253
|
}
|
|
7815
9254
|
catch {
|
|
7816
9255
|
return null;
|
|
7817
9256
|
}
|
|
7818
9257
|
}
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
9258
|
+
function walkCoverageFiles(repoPath, intent) {
|
|
9259
|
+
if (intent.mode === "full")
|
|
9260
|
+
return walkRepoFiles(repoPath);
|
|
9261
|
+
const files = new Set();
|
|
9262
|
+
for (const file of intent.files)
|
|
9263
|
+
if (safeCoveragePath(repoPath, file)?.isFile())
|
|
9264
|
+
files.add(file);
|
|
9265
|
+
for (const prefix of intent.prefixes) {
|
|
9266
|
+
const stat = safeCoveragePath(repoPath, prefix);
|
|
9267
|
+
if (stat?.isFile())
|
|
9268
|
+
files.add(prefix);
|
|
9269
|
+
else if (stat?.isDirectory())
|
|
9270
|
+
for (const file of walkRepoFiles(path.join(repoPath, prefix)))
|
|
9271
|
+
files.add(`${prefix}/${file}`);
|
|
7823
9272
|
}
|
|
7824
|
-
|
|
9273
|
+
return [...files].sort(compareBytes);
|
|
7825
9274
|
}
|
|
7826
9275
|
function commitRelation(repoPath, indexedHead, currentHead) {
|
|
7827
9276
|
if (!indexedHead || !currentHead)
|
|
@@ -7856,16 +9305,21 @@ function freshnessFingerprint(value) {
|
|
|
7856
9305
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
7857
9306
|
}
|
|
7858
9307
|
function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride, gitProbe) {
|
|
7859
|
-
const
|
|
9308
|
+
const coverage = coverageForDatabase(db);
|
|
9309
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath, coverage.intent ?? undefined) : gitProbe;
|
|
9310
|
+
const intent = coverage.intent;
|
|
9311
|
+
const scopedDirty = intent
|
|
9312
|
+
? git?.dirty.filter((file) => coverageContains(intent, file))
|
|
9313
|
+
: undefined;
|
|
7860
9314
|
const currentHead = git && isSameDir(git.root, repoPath) ? git.head || null : gitHead(repoPath);
|
|
7861
9315
|
const indexedHead = db ? getMeta(db, "lastIndexedHead") || null : null;
|
|
7862
9316
|
const relation = commitRelation(repoPath, indexedHead, currentHead);
|
|
7863
9317
|
const baseline = db ? getMeta(db, "lastProbeDirty") : null;
|
|
7864
|
-
const pendingPaths = git && isSameDir(git.root, repoPath) ?
|
|
9318
|
+
const pendingPaths = git && isSameDir(git.root, repoPath) ? (scopedDirty?.length ?? null) : null;
|
|
7865
9319
|
const workingTreeChanged = git !== null &&
|
|
7866
9320
|
isSameDir(git.root, repoPath) &&
|
|
7867
9321
|
baseline !== null &&
|
|
7868
|
-
baseline !==
|
|
9322
|
+
baseline !== (scopedDirty ? coverageDirtyFingerprint(coverage.intent, scopedDirty) : undefined);
|
|
7869
9323
|
const state = stateOverride ??
|
|
7870
9324
|
(currentHead === null && indexedHead === null && baseline !== null
|
|
7871
9325
|
? workingTreeChanged
|
|
@@ -7880,6 +9334,7 @@ function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride, gitProb
|
|
|
7880
9334
|
: "fresh");
|
|
7881
9335
|
return {
|
|
7882
9336
|
currentHead,
|
|
9337
|
+
indexCoverage: indexCoverageSummary(coverage),
|
|
7883
9338
|
indexedHead,
|
|
7884
9339
|
...relation,
|
|
7885
9340
|
workingTree: {
|
|
@@ -7963,6 +9418,10 @@ function removeIndexState(db, relPath) {
|
|
|
7963
9418
|
if (!shouldPurgeMissingFile(db))
|
|
7964
9419
|
return;
|
|
7965
9420
|
try {
|
|
9421
|
+
db.run("DELETE FROM apex_consumer_files WHERE filePath = ?", [relPath]);
|
|
9422
|
+
db.run("DELETE FROM apex_class_uses WHERE filePath = ?", [relPath]);
|
|
9423
|
+
if (/\.(?:cls|trigger)$/i.test(relPath))
|
|
9424
|
+
setMeta(db, "apexConsumerFactsRevision", crypto.randomUUID());
|
|
7966
9425
|
db.run("DELETE FROM index_state WHERE filePath = ?", [relPath]);
|
|
7967
9426
|
}
|
|
7968
9427
|
catch (_) { }
|
|
@@ -8035,8 +9494,9 @@ function detectDriftByStat(repoPath, db, canonicalFiles) {
|
|
|
8035
9494
|
}
|
|
8036
9495
|
return changed;
|
|
8037
9496
|
}
|
|
8038
|
-
async function reconcileIndex(repoPath, db, progress, skipEmbeddings =
|
|
9497
|
+
async function reconcileIndex(repoPath, db, progress, skipEmbeddings = getMeta(db, "semanticDeferred") === "1") {
|
|
8039
9498
|
try {
|
|
9499
|
+
const coverage = requireIndexCoverage(coverageForDatabase(db));
|
|
8040
9500
|
progress?.("collecting-files", 0, "Checking existing local graph for changes");
|
|
8041
9501
|
const changed = new Set();
|
|
8042
9502
|
const head = gitHead(repoPath);
|
|
@@ -8045,26 +9505,37 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8045
9505
|
// --- git fast path ---
|
|
8046
9506
|
const lastHead = getMeta(db, "lastIndexedHead");
|
|
8047
9507
|
try {
|
|
8048
|
-
const
|
|
8049
|
-
|
|
8050
|
-
encoding: "utf-8",
|
|
8051
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
8052
|
-
});
|
|
8053
|
-
for (const file of parseGitStatusPaths(status))
|
|
8054
|
-
changed.add(file);
|
|
8055
|
-
}
|
|
8056
|
-
catch (_) { }
|
|
8057
|
-
let baselineUnresolved = false;
|
|
8058
|
-
if (lastHead && lastHead !== head) {
|
|
8059
|
-
try {
|
|
8060
|
-
const diff = child_process.execFileSync("git", ["diff", "--name-only", "-z", lastHead, head, "--"], {
|
|
9508
|
+
for (const pathspecs of coverageGitPathspecBatches(coverage)) {
|
|
9509
|
+
const status = child_process.execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", ...pathspecs], {
|
|
8061
9510
|
cwd: repoPath,
|
|
8062
9511
|
encoding: "utf-8",
|
|
8063
9512
|
stdio: ["ignore", "pipe", "ignore"],
|
|
8064
9513
|
});
|
|
8065
|
-
for (const file of
|
|
9514
|
+
for (const file of parseGitStatusPaths(status))
|
|
8066
9515
|
changed.add(file);
|
|
8067
9516
|
}
|
|
9517
|
+
}
|
|
9518
|
+
catch (_) { }
|
|
9519
|
+
let baselineUnresolved = false;
|
|
9520
|
+
if (lastHead && lastHead !== head) {
|
|
9521
|
+
try {
|
|
9522
|
+
for (const pathspecs of coverageGitPathspecBatches(coverage)) {
|
|
9523
|
+
const diff = child_process.execFileSync("git", [
|
|
9524
|
+
"diff",
|
|
9525
|
+
"--name-only",
|
|
9526
|
+
"-z",
|
|
9527
|
+
lastHead,
|
|
9528
|
+
head,
|
|
9529
|
+
...(pathspecs.length ? pathspecs : ["--"]),
|
|
9530
|
+
], {
|
|
9531
|
+
cwd: repoPath,
|
|
9532
|
+
encoding: "utf-8",
|
|
9533
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9534
|
+
});
|
|
9535
|
+
for (const file of splitNulPaths(diff))
|
|
9536
|
+
changed.add(file);
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
8068
9539
|
catch (_) {
|
|
8069
9540
|
// `lastHead` is not resolvable in this checkout's object database
|
|
8070
9541
|
// (a shallow clone missing that commit, or a database seeded from
|
|
@@ -8077,7 +9548,7 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8077
9548
|
}
|
|
8078
9549
|
if (baselineUnresolved) {
|
|
8079
9550
|
console.warn(`knodin: cached baseline commit ${lastHead} is not resolvable in this checkout (shallow clone or foreign database) — falling back to a full file-drift comparison`);
|
|
8080
|
-
canonicalFiles = collectRepoFiles(repoPath);
|
|
9551
|
+
canonicalFiles = collectRepoFiles(repoPath, db);
|
|
8081
9552
|
if (canonicalFiles.length > 50000) {
|
|
8082
9553
|
console.warn(`knodin: skipping full-drift fallback reconcile (${canonicalFiles.length} files exceeds 50k bound)`);
|
|
8083
9554
|
}
|
|
@@ -8089,7 +9560,7 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8089
9560
|
}
|
|
8090
9561
|
else {
|
|
8091
9562
|
// --- non-git fallback: mtime/size vs index_state ---
|
|
8092
|
-
canonicalFiles = collectRepoFiles(repoPath);
|
|
9563
|
+
canonicalFiles = collectRepoFiles(repoPath, db);
|
|
8093
9564
|
if (canonicalFiles.length > 50000) {
|
|
8094
9565
|
console.warn(`knodin: skipping freshness reconcile (${canonicalFiles.length} files exceeds 50k bound)`);
|
|
8095
9566
|
return 0;
|
|
@@ -8100,7 +9571,7 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8100
9571
|
// A row in index_state whose file is gone or no longer satisfies the
|
|
8101
9572
|
// canonical source policy must be purged. Keep those legacy rows in the
|
|
8102
9573
|
// candidate set without admitting newly changed non-source files.
|
|
8103
|
-
canonicalFiles ??= collectRepoFiles(repoPath);
|
|
9574
|
+
canonicalFiles ??= collectRepoFiles(repoPath, db);
|
|
8104
9575
|
const canonicalPaths = new Set(canonicalFiles);
|
|
8105
9576
|
const indexedPaths = new Set();
|
|
8106
9577
|
for (const row of db
|
|
@@ -8114,7 +9585,8 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8114
9585
|
if (!indexedPaths.has(file))
|
|
8115
9586
|
changed.add(file);
|
|
8116
9587
|
const candidates = [...changed]
|
|
8117
|
-
.filter((file) =>
|
|
9588
|
+
.filter((file) => (coverageContains(coverage, file) && isIndexableSourcePath(file)) ||
|
|
9589
|
+
indexedPaths.has(file))
|
|
8118
9590
|
.sort(compareBytes);
|
|
8119
9591
|
progress?.("indexing-files", 0, `Reconciling ${candidates.length.toLocaleString()} changed files`, {
|
|
8120
9592
|
phaseTotal: candidates.length,
|
|
@@ -8154,6 +9626,10 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8154
9626
|
// Re-embed ONLY what changed (indexEmbeddings embeds symbols lacking an
|
|
8155
9627
|
// embedding), so a no-op reconcile triggers no embedding work at all.
|
|
8156
9628
|
if (reindexed > 0) {
|
|
9629
|
+
const refreshed = await refreshFlowsForApexFiles(db, repoPath, reindexedPaths);
|
|
9630
|
+
for (const file of refreshed)
|
|
9631
|
+
if (!reindexedPaths.includes(file))
|
|
9632
|
+
reindexedPaths.push(file);
|
|
8157
9633
|
progress?.("finalizing", 0, skipEmbeddings
|
|
8158
9634
|
? "Refreshing identities (semantic embeddings deferred)"
|
|
8159
9635
|
: "Refreshing identities and semantic embeddings");
|
|
@@ -8186,6 +9662,7 @@ async function reconcileIndex(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
8186
9662
|
setMeta(db, "knodinVersion", KNODIN_VERSION);
|
|
8187
9663
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
8188
9664
|
recordFreshnessBaseline(repoPath, db);
|
|
9665
|
+
completeCoverageIntent(db);
|
|
8189
9666
|
return reindexed;
|
|
8190
9667
|
}
|
|
8191
9668
|
catch (e) {
|
|
@@ -8309,7 +9786,35 @@ export function resetFreshnessStats() {
|
|
|
8309
9786
|
* Returns null when `repoPath` is not a git work tree (or git failed), which
|
|
8310
9787
|
* sends the caller to the mtime fallback.
|
|
8311
9788
|
*/
|
|
8312
|
-
function gitWorkTreeProbe(repoPath) {
|
|
9789
|
+
function gitWorkTreeProbe(repoPath, intent) {
|
|
9790
|
+
if (intent?.mode === "scoped") {
|
|
9791
|
+
try {
|
|
9792
|
+
const options = {
|
|
9793
|
+
cwd: repoPath,
|
|
9794
|
+
encoding: "utf8",
|
|
9795
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9796
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
9797
|
+
};
|
|
9798
|
+
const root = child_process
|
|
9799
|
+
.execFileSync("git", ["rev-parse", "--show-toplevel"], options)
|
|
9800
|
+
.trim();
|
|
9801
|
+
if (!isSameDir(root, repoPath))
|
|
9802
|
+
return null;
|
|
9803
|
+
const dirty = new Set();
|
|
9804
|
+
for (const pathspecs of coverageGitPathspecBatches(intent)) {
|
|
9805
|
+
const output = child_process.execFileSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all", ...pathspecs], options);
|
|
9806
|
+
for (const file of parseGitStatusPaths(output))
|
|
9807
|
+
if (coverageContains(intent, file) && isIndexablePath(file))
|
|
9808
|
+
dirty.add(file);
|
|
9809
|
+
}
|
|
9810
|
+
return { root, head: gitHead(repoPath) ?? "", dirty: [...dirty].sort(compareBytes) };
|
|
9811
|
+
}
|
|
9812
|
+
catch (error) {
|
|
9813
|
+
if (error.code === "ENOBUFS")
|
|
9814
|
+
freshnessStats.probeOverflows++;
|
|
9815
|
+
return null;
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
8313
9818
|
let lines;
|
|
8314
9819
|
try {
|
|
8315
9820
|
lines = child_process
|
|
@@ -8385,10 +9890,17 @@ function fileDriftedFromIndexState(db, repoPath, relPath) {
|
|
|
8385
9890
|
* compare the dirty set against. Uses the existing R8 `meta` table — no schema
|
|
8386
9891
|
* bump — and self-heals on indexes written before this key existed.
|
|
8387
9892
|
*/
|
|
9893
|
+
function coverageDirtyFingerprint(intent, dirty) {
|
|
9894
|
+
return intent?.mode === "scoped" ? JSON.stringify(dirty) : dirty.join("\n");
|
|
9895
|
+
}
|
|
8388
9896
|
function recordFreshnessBaseline(repoPath, db) {
|
|
8389
|
-
const
|
|
8390
|
-
const
|
|
8391
|
-
|
|
9897
|
+
const intent = requireIndexCoverage(coverageForDatabase(db));
|
|
9898
|
+
const git = gitWorkTreeProbe(repoPath, intent);
|
|
9899
|
+
const dirty = git && isSameDir(git.root, repoPath)
|
|
9900
|
+
? git.dirty.filter((file) => coverageContains(intent, file))
|
|
9901
|
+
: null;
|
|
9902
|
+
setMeta(db, "lastProbeDirty", dirty === null ? "" : coverageDirtyFingerprint(intent, dirty));
|
|
9903
|
+
setMeta(db, "lastProbeCoverage", indexCoverageSummary(coverageForDatabase(db)).digest ?? "");
|
|
8392
9904
|
}
|
|
8393
9905
|
/**
|
|
8394
9906
|
* Decide — WITHOUT indexing anything — whether the index still matches the
|
|
@@ -8411,15 +9923,24 @@ function recordFreshnessBaseline(repoPath, db) {
|
|
|
8411
9923
|
* the watcher or a cold start, not by this probe.
|
|
8412
9924
|
*/
|
|
8413
9925
|
function probeDrift(repoPath, db, gitProbe) {
|
|
8414
|
-
const
|
|
9926
|
+
const coverage = coverageForDatabase(db);
|
|
9927
|
+
const intent = requireIndexCoverage(coverage);
|
|
9928
|
+
if (coverage.intentState === "pending" ||
|
|
9929
|
+
(coverage.origin === "explicit" &&
|
|
9930
|
+
getMeta(db, "lastProbeCoverage") !== indexCoverageSummary(coverage).digest))
|
|
9931
|
+
return { drifted: true, verifiable: true };
|
|
9932
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath, intent) : gitProbe;
|
|
9933
|
+
if (!git && intent.mode === "scoped" && fs.existsSync(path.join(repoPath, ".git")))
|
|
9934
|
+
return { drifted: false, verifiable: false };
|
|
8415
9935
|
if (git && isSameDir(git.root, repoPath)) {
|
|
8416
9936
|
if ((getMeta(db, "lastIndexedHead") ?? "") !== git.head) {
|
|
8417
9937
|
return { drifted: true, verifiable: true };
|
|
8418
9938
|
}
|
|
8419
|
-
|
|
9939
|
+
const dirty = git.dirty.filter((file) => coverageContains(intent, file));
|
|
9940
|
+
if (getMeta(db, "lastProbeDirty") !== coverageDirtyFingerprint(intent, dirty)) {
|
|
8420
9941
|
return { drifted: true, verifiable: true };
|
|
8421
9942
|
}
|
|
8422
|
-
for (const rel of
|
|
9943
|
+
for (const rel of dirty) {
|
|
8423
9944
|
if (!isIndexablePath(rel))
|
|
8424
9945
|
continue;
|
|
8425
9946
|
if (fileDriftedFromIndexState(db, repoPath, rel))
|
|
@@ -8456,6 +9977,8 @@ function noteFreshness(repoPath, staleness) {
|
|
|
8456
9977
|
* after the fact is therefore worthless here.
|
|
8457
9978
|
*/
|
|
8458
9979
|
async function ensureIndexFresh(repoPath, db) {
|
|
9980
|
+
const coverage = coverageForDatabase(db);
|
|
9981
|
+
requireIndexCoverage(coverage);
|
|
8459
9982
|
const key = path.resolve(repoPath);
|
|
8460
9983
|
const cached = freshnessProbes.get(key);
|
|
8461
9984
|
// A watcher cannot prove that an edit made immediately before this call has
|
|
@@ -8475,7 +9998,11 @@ async function ensureIndexFresh(repoPath, db) {
|
|
|
8475
9998
|
: watchQueues.get(key)?.ready
|
|
8476
9999
|
? WATCHED_FRESHNESS_LEASE_MS
|
|
8477
10000
|
: FRESHNESS_PROBE_TTL_MS;
|
|
8478
|
-
if (cached &&
|
|
10001
|
+
if (cached &&
|
|
10002
|
+
Date.now() - cached.at < lease &&
|
|
10003
|
+
coverage.intentState !== "pending" &&
|
|
10004
|
+
(coverage.origin !== "explicit" ||
|
|
10005
|
+
getMeta(db, "lastProbeCoverage") === indexCoverageSummary(coverage).digest)) {
|
|
8479
10006
|
freshnessStats.cacheHits++;
|
|
8480
10007
|
return cached.staleness;
|
|
8481
10008
|
}
|
|
@@ -8522,7 +10049,7 @@ function stalenessFor(repoPath) {
|
|
|
8522
10049
|
return freshnessProbes.get(path.resolve(repoPath))?.staleness ?? "unknown";
|
|
8523
10050
|
}
|
|
8524
10051
|
/** Recursively indexes all matching files within the repository. */
|
|
8525
|
-
async function indexRepo(repoPath, db, progress, skipEmbeddings =
|
|
10052
|
+
async function indexRepo(repoPath, db, progress, skipEmbeddings = getMeta(db, "semanticDeferred") === "1", publishProcessState = true,
|
|
8526
10053
|
/**
|
|
8527
10054
|
* Continue a build already in this database instead of starting over.
|
|
8528
10055
|
*
|
|
@@ -8533,8 +10060,9 @@ async function indexRepo(repoPath, db, progress, skipEmbeddings = false, publish
|
|
|
8533
10060
|
* observable in that condition.
|
|
8534
10061
|
*/
|
|
8535
10062
|
resume = false) {
|
|
10063
|
+
setMeta(db, "semanticDeferred", skipEmbeddings ? "1" : "0");
|
|
8536
10064
|
progress?.("collecting-files", 0, "Discovering indexable files");
|
|
8537
|
-
const collected = measurePerfPhaseSync("file_collection", () => collectRepoFilesWithCoverage(repoPath));
|
|
10065
|
+
const collected = measurePerfPhaseSync("file_collection", () => collectRepoFilesWithCoverage(repoPath, db));
|
|
8538
10066
|
const files = collected.files;
|
|
8539
10067
|
// Files that reach the indexer but yield nothing can only be observed here,
|
|
8540
10068
|
// during the parse. Persisted below so `status` can report the gap without
|
|
@@ -8582,6 +10110,9 @@ resume = false) {
|
|
|
8582
10110
|
db.run("DELETE FROM dependencies;");
|
|
8583
10111
|
db.run("DELETE FROM mcp_tools;");
|
|
8584
10112
|
db.run("DELETE FROM index_state;");
|
|
10113
|
+
db.run("DELETE FROM apex_consumer_files;");
|
|
10114
|
+
db.run("DELETE FROM apex_class_uses;");
|
|
10115
|
+
setMeta(db, "apexConsumerFactsRevision", crypto.randomUUID());
|
|
8585
10116
|
}
|
|
8586
10117
|
// On a resume, everything already recorded and still matching disk is done.
|
|
8587
10118
|
// Counted toward progress rather than dropped from it, so the totals stay
|
|
@@ -8684,6 +10215,8 @@ resume = false) {
|
|
|
8684
10215
|
await pool.run(eligible, async (task, outcome) => {
|
|
8685
10216
|
switch (outcome.kind) {
|
|
8686
10217
|
case "result":
|
|
10218
|
+
if (outcome.result.apexClassUseIncompleteReason)
|
|
10219
|
+
tallyOne(unparsedByExtension, extensionBucket(task.relativePath));
|
|
8687
10220
|
pendingWrites.push(outcome.result);
|
|
8688
10221
|
break;
|
|
8689
10222
|
case "unparsed":
|
|
@@ -8717,6 +10250,7 @@ resume = false) {
|
|
|
8717
10250
|
}
|
|
8718
10251
|
flushPendingWrites();
|
|
8719
10252
|
progress?.("finalizing", 0, "Persisting symbol identities and semantic index");
|
|
10253
|
+
await refreshFlowsForApexFiles(db, repoPath, files);
|
|
8720
10254
|
persistSymbolIdentities(db, repoPath);
|
|
8721
10255
|
reconcileTypeScriptDi(db, repoPath);
|
|
8722
10256
|
// Structure is complete at this point; embeddings are the expensive tail. When
|
|
@@ -8816,11 +10350,14 @@ function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_F
|
|
|
8816
10350
|
}
|
|
8817
10351
|
throw new Error("injected watcher flush failure");
|
|
8818
10352
|
}
|
|
10353
|
+
const intent = requireIndexCoverage(coverageForDatabase(db));
|
|
10354
|
+
const canonicalScopedFiles = intent.mode === "scoped" ? new Set(collectRepoFiles(resolvedRepoPath, db)) : null;
|
|
8819
10355
|
for (const relativePath of paths) {
|
|
8820
10356
|
const absolutePath = path.join(resolvedRepoPath, relativePath);
|
|
8821
10357
|
// Event order is intentionally ignored: disk truth at flush time handles
|
|
8822
10358
|
// atomic-save unlink/add sequences and duplicate change notifications.
|
|
8823
|
-
if (fs.existsSync(absolutePath)
|
|
10359
|
+
if (fs.existsSync(absolutePath) &&
|
|
10360
|
+
(!canonicalScopedFiles || canonicalScopedFiles.has(relativePath))) {
|
|
8824
10361
|
await indexFile(absolutePath, relativePath, resolvedRepoPath, db);
|
|
8825
10362
|
recordIndexState(db, resolvedRepoPath, relativePath);
|
|
8826
10363
|
}
|
|
@@ -8828,10 +10365,13 @@ function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_F
|
|
|
8828
10365
|
purgeFile(relativePath);
|
|
8829
10366
|
}
|
|
8830
10367
|
}
|
|
8831
|
-
|
|
10368
|
+
const refreshed = await refreshFlowsForApexFiles(db, resolvedRepoPath, paths);
|
|
10369
|
+
persistSymbolIdentities(db, resolvedRepoPath, [...new Set([...paths, ...refreshed])]);
|
|
8832
10370
|
reconcileTypeScriptDi(db, resolvedRepoPath);
|
|
8833
|
-
|
|
8834
|
-
|
|
10371
|
+
if (getMeta(db, "semanticDeferred") !== "1") {
|
|
10372
|
+
await indexEmbeddings(db, resolvedRepoPath);
|
|
10373
|
+
queue.embeddingReconciliations++;
|
|
10374
|
+
}
|
|
8835
10375
|
indexGeneration++;
|
|
8836
10376
|
queue.generationAdvances++;
|
|
8837
10377
|
recordFreshnessBaseline(resolvedRepoPath, db);
|
|
@@ -8871,7 +10411,10 @@ function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_F
|
|
|
8871
10411
|
if (queue.closed)
|
|
8872
10412
|
return;
|
|
8873
10413
|
const relativePath = path.relative(resolvedRepoPath, filePath);
|
|
8874
|
-
|
|
10414
|
+
const coverage = coverageForDatabase(db);
|
|
10415
|
+
if (!coverage.intent ||
|
|
10416
|
+
!coverageContains(coverage.intent, relativePath) ||
|
|
10417
|
+
!isIndexableSourcePath(relativePath))
|
|
8875
10418
|
return;
|
|
8876
10419
|
freshnessProbes.delete(resolvedRepoPath);
|
|
8877
10420
|
queue.pending.add(relativePath);
|
|
@@ -8884,7 +10427,13 @@ function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_F
|
|
|
8884
10427
|
};
|
|
8885
10428
|
queue.drain = flush;
|
|
8886
10429
|
const watcher = chokidar.watch(repoPath, {
|
|
8887
|
-
ignored:
|
|
10430
|
+
ignored: (targetPath) => {
|
|
10431
|
+
const coverage = coverageForDatabase(db);
|
|
10432
|
+
const relative = path.relative(repoPath, targetPath).split(path.sep).join("/");
|
|
10433
|
+
return (!coverage.intent ||
|
|
10434
|
+
!coverageMayContain(coverage.intent, relative) ||
|
|
10435
|
+
makeWatchIgnorePredicate(repoPath)(targetPath));
|
|
10436
|
+
},
|
|
8888
10437
|
persistent: true,
|
|
8889
10438
|
ignoreInitial: true,
|
|
8890
10439
|
});
|
|
@@ -9103,6 +10652,10 @@ export async function getOrInitDb(repoPath, options = {}) {
|
|
|
9103
10652
|
db.run("ALTER TABLE symbols ADD COLUMN identity TEXT;");
|
|
9104
10653
|
}
|
|
9105
10654
|
catch (_) { }
|
|
10655
|
+
try {
|
|
10656
|
+
db.run("ALTER TABLE symbols ADD COLUMN apexDeclaration TEXT;");
|
|
10657
|
+
}
|
|
10658
|
+
catch (_) { }
|
|
9106
10659
|
db.run("CREATE INDEX IF NOT EXISTS idx_symbols_identity ON symbols(identity);");
|
|
9107
10660
|
db.run(`
|
|
9108
10661
|
CREATE TABLE IF NOT EXISTS "references" (
|
|
@@ -9153,6 +10706,20 @@ export async function getOrInitDb(repoPath, options = {}) {
|
|
|
9153
10706
|
db.run('ALTER TABLE "references" ADD COLUMN calleeIdentity TEXT;');
|
|
9154
10707
|
}
|
|
9155
10708
|
catch (_) { }
|
|
10709
|
+
try {
|
|
10710
|
+
db.run('ALTER TABLE "references" ADD COLUMN apexCall TEXT;');
|
|
10711
|
+
}
|
|
10712
|
+
catch (_) { }
|
|
10713
|
+
db.run("CREATE TABLE IF NOT EXISTS apex_file_capabilities(filePath TEXT PRIMARY KEY, version INTEGER NOT NULL)");
|
|
10714
|
+
db.run("CREATE TABLE IF NOT EXISTS apex_consumer_files(filePath TEXT PRIMARY KEY, version INTEGER NOT NULL, sourceHash TEXT NOT NULL, parseState TEXT NOT NULL, isTest INTEGER, facts TEXT NOT NULL, factsHash TEXT)");
|
|
10715
|
+
try {
|
|
10716
|
+
db.run("ALTER TABLE apex_consumer_files ADD COLUMN factsHash TEXT");
|
|
10717
|
+
}
|
|
10718
|
+
catch (_) { }
|
|
10719
|
+
db.run("CREATE TABLE IF NOT EXISTS apex_class_uses(filePath TEXT NOT NULL,startByte INTEGER NOT NULL,endByte INTEGER NOT NULL,kind TEXT NOT NULL,raw TEXT NOT NULL,status TEXT NOT NULL,targetFile TEXT,targetOwner TEXT,targetIdentity TEXT,ownerIdentity TEXT,isTest INTEGER,PRIMARY KEY(filePath,startByte,endByte,kind))");
|
|
10720
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_apex_class_uses_target ON apex_class_uses(targetFile,targetOwner,filePath,startByte,endByte,kind)");
|
|
10721
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_symbols_apex_provenance ON symbols(id) WHERE apexDeclaration IS NOT NULL");
|
|
10722
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_references_apex_provenance ON "references"(id) WHERE apexCall IS NOT NULL');
|
|
9156
10723
|
db.run(`
|
|
9157
10724
|
CREATE TABLE IF NOT EXISTS dependencies (
|
|
9158
10725
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -9314,12 +10881,16 @@ export async function getOrInitDb(repoPath, options = {}) {
|
|
|
9314
10881
|
catch {
|
|
9315
10882
|
needsIndex = true;
|
|
9316
10883
|
}
|
|
9317
|
-
if (
|
|
10884
|
+
if (!options.skipContentReconciliation)
|
|
10885
|
+
requireIndexCoverage(coverageForDatabase(db));
|
|
10886
|
+
if (needsIndex &&
|
|
10887
|
+
coverageForDatabase(db).intent?.mode !== "scoped" &&
|
|
10888
|
+
!options.skipContentReconciliation) {
|
|
9318
10889
|
await indexRepo(normalizedPath, db, options.onProgress);
|
|
9319
10890
|
// Just built from the working tree — nothing can be stale yet.
|
|
9320
10891
|
noteFreshness(normalizedPath, "fresh");
|
|
9321
10892
|
}
|
|
9322
|
-
else if (!
|
|
10893
|
+
else if (!options.skipContentReconciliation) {
|
|
9323
10894
|
// Existing on-disk index: reconcile any offline drift (git pull /
|
|
9324
10895
|
// branch switch / rebase / external edit) before serving queries.
|
|
9325
10896
|
const reindexed = await reconcileIndex(normalizedPath, db, options.onProgress);
|
|
@@ -9584,7 +11155,11 @@ function listSourceFiles(repoPath, accept) {
|
|
|
9584
11155
|
const sealed = sealedRepositories.get(path.resolve(repoPath));
|
|
9585
11156
|
if (sealed)
|
|
9586
11157
|
return listSealedFiles(sealed).filter(accept);
|
|
9587
|
-
|
|
11158
|
+
const db = extractionCoverage.getStore() ?? dbInstances.get(path.resolve(repoPath));
|
|
11159
|
+
const intent = requireIndexCoverage(coverageForDatabase(db));
|
|
11160
|
+
return intent.mode === "full"
|
|
11161
|
+
? walkRepoFiles(repoPath, { accept })
|
|
11162
|
+
: (gitTrackedCandidates(repoPath, intent) ?? walkCoverageFiles(repoPath, intent)).filter((file) => isIndexablePath(file) && accept(file));
|
|
9588
11163
|
}
|
|
9589
11164
|
/** Get verbatim source with line numbers, and say so when it is unavailable. */
|
|
9590
11165
|
function readSourceRange(repoPath, relativePath, startLine, endLine, maxLines) {
|
|
@@ -10040,7 +11615,7 @@ const BUILTIN_CALLEE_NAMES = new Set([
|
|
|
10040
11615
|
* unrelated same-named symbol. A caller's own definition lives in its
|
|
10041
11616
|
* `callerFile`, so recursion stays file-qualified by passing that down.
|
|
10042
11617
|
*/
|
|
10043
|
-
function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, visited = new Set(), targetFile = null, targetIdentity = null) {
|
|
11618
|
+
function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, visited = new Set(), targetFile = null, targetIdentity = null, includeAnonymousOrigins = false) {
|
|
10044
11619
|
if (depth <= 0)
|
|
10045
11620
|
return [];
|
|
10046
11621
|
const visitedKey = `${currentRepoPath}:${targetSymbol}:${targetFile ?? ""}`;
|
|
@@ -10062,28 +11637,32 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
10062
11637
|
FROM "references"
|
|
10063
11638
|
WHERE calleeSymbol = ? AND calleeFile = ?
|
|
10064
11639
|
AND (? IS NULL OR calleeIdentity = ?)
|
|
10065
|
-
AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
11640
|
+
AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action', 'apex_method')
|
|
10066
11641
|
`);
|
|
10067
11642
|
rows = stmt.all(targetSymbol, targetFile, targetIdentity, targetIdentity);
|
|
10068
11643
|
stmt.finalize();
|
|
10069
11644
|
}
|
|
10070
11645
|
else {
|
|
10071
11646
|
const stmt = db.query(`
|
|
10072
|
-
SELECT
|
|
11647
|
+
SELECT *
|
|
10073
11648
|
FROM "references"
|
|
10074
|
-
WHERE calleeSymbol = ? AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
11649
|
+
WHERE calleeSymbol = ? AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action', 'apex_method')
|
|
10075
11650
|
`);
|
|
10076
11651
|
rows = stmt.all(targetSymbol);
|
|
10077
11652
|
stmt.finalize();
|
|
10078
11653
|
}
|
|
11654
|
+
const validApexReference = apexReferenceReadGuard(db);
|
|
10079
11655
|
for (const row of rows) {
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
//
|
|
10083
|
-
|
|
11656
|
+
if (!validApexReference(row))
|
|
11657
|
+
continue;
|
|
11658
|
+
// Preserve exact callee-resolved sites even when no named caller was
|
|
11659
|
+
// extracted. The label is not a symbol identity or a claim about nesting.
|
|
11660
|
+
const anonymous = row.callerSymbol === null;
|
|
11661
|
+
if (anonymous && !includeAnonymousOrigins)
|
|
10084
11662
|
continue;
|
|
10085
11663
|
results.push({
|
|
10086
|
-
symbol: row.callerSymbol,
|
|
11664
|
+
symbol: row.callerSymbol ?? "(anonymous or top-level)",
|
|
11665
|
+
...(anonymous ? { callerOrigin: "anonymous-or-top-level" } : {}),
|
|
10087
11666
|
filePath: row.callerFile,
|
|
10088
11667
|
lineNumber: row.line,
|
|
10089
11668
|
repoPath: currentRepoPath,
|
|
@@ -10092,6 +11671,8 @@ function findCallersFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
10092
11671
|
evidenceFile: row.callerFile,
|
|
10093
11672
|
evidenceLine: row.line,
|
|
10094
11673
|
});
|
|
11674
|
+
if (!row.callerSymbol)
|
|
11675
|
+
continue;
|
|
10095
11676
|
const parentCallers = findCallersFederated(allRepos, currentRepoPath, row.callerSymbol, depth - 1, visited, row.callerFile, row.callerIdentity ?? null);
|
|
10096
11677
|
results.push(...parentCallers);
|
|
10097
11678
|
}
|
|
@@ -10172,14 +11753,17 @@ function findCalleesFederated(allRepos, currentRepoPath, targetSymbol, depth, vi
|
|
|
10172
11753
|
}
|
|
10173
11754
|
else {
|
|
10174
11755
|
const stmt = db.query(`
|
|
10175
|
-
SELECT
|
|
11756
|
+
SELECT *
|
|
10176
11757
|
FROM "references"
|
|
10177
11758
|
WHERE callerSymbol = ? AND kind IN ('call', 'di_binding', 'di_resolution', 'flow_apex_action')
|
|
10178
11759
|
`);
|
|
10179
11760
|
rows = stmt.all(targetSymbol);
|
|
10180
11761
|
stmt.finalize();
|
|
10181
11762
|
}
|
|
11763
|
+
const validApexReference = apexReferenceReadGuard(db);
|
|
10182
11764
|
for (const row of rows) {
|
|
11765
|
+
if (!validApexReference(row))
|
|
11766
|
+
continue;
|
|
10183
11767
|
// An unresolved callee (no calleeFile) with a built-in / global name is
|
|
10184
11768
|
// almost certainly a stdlib member call (Math.min -> "min") — skip it even
|
|
10185
11769
|
// if some unrelated project symbol happens to share the name, so it doesn't
|
|
@@ -11837,6 +13421,7 @@ function withStaleness(engine, claimRepository, openDb, openPolicy) {
|
|
|
11837
13421
|
async status(repoPath, options) {
|
|
11838
13422
|
claimRepository(repoPath);
|
|
11839
13423
|
const result = await engine.status(repoPath, options);
|
|
13424
|
+
result.indexCoverage ??= result.freshness.indexCoverage;
|
|
11840
13425
|
result.freshnessMechanism = freshnessMechanismFor(repoPath, openPolicy);
|
|
11841
13426
|
return result;
|
|
11842
13427
|
},
|
|
@@ -11864,10 +13449,15 @@ function withStaleness(engine, claimRepository, openDb, openPolicy) {
|
|
|
11864
13449
|
// stamping them with the index's freshness would misreport them.
|
|
11865
13450
|
return engine.searchText(term, repoPath, options);
|
|
11866
13451
|
},
|
|
13452
|
+
queryClassConsumers(repoPath, request) {
|
|
13453
|
+
claimRepository(repoPath);
|
|
13454
|
+
return engine.queryClassConsumers(repoPath, request);
|
|
13455
|
+
},
|
|
11867
13456
|
async query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options) {
|
|
11868
13457
|
claimRepository(repoPath);
|
|
11869
13458
|
const result = await engine.query(pattern, target, repoPath, to, limit, depth, detailLevel, selector, impactOptions, options);
|
|
11870
|
-
|
|
13459
|
+
if (pattern !== "class_consumers")
|
|
13460
|
+
result.staleness = stalenessFor(repoPath);
|
|
11871
13461
|
return result;
|
|
11872
13462
|
},
|
|
11873
13463
|
async rename(oldName, newName, repoPath, apply, verify, selector) {
|
|
@@ -12178,6 +13768,14 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12178
13768
|
if (!db)
|
|
12179
13769
|
throw new Error("candidate database did not open");
|
|
12180
13770
|
if (!changes) {
|
|
13771
|
+
const oldCoverage = coverageForDatabase(db);
|
|
13772
|
+
writeCoverageIntent(db, resolved, {
|
|
13773
|
+
version: 1,
|
|
13774
|
+
mode: "full",
|
|
13775
|
+
revision: (oldCoverage.intent?.revision ?? 0) + 1,
|
|
13776
|
+
prefixes: [],
|
|
13777
|
+
files: [],
|
|
13778
|
+
});
|
|
12181
13779
|
// Claim the build before touching anything, and record which tree it
|
|
12182
13780
|
// is for. A candidate carrying this marker is one a previous run left
|
|
12183
13781
|
// mid-build; without the commit, a resume could silently continue a
|
|
@@ -12190,11 +13788,12 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12190
13788
|
// killed above leaves it set, which is exactly what makes the
|
|
12191
13789
|
// candidate recognisable as resumable rather than abandoned.
|
|
12192
13790
|
setMeta(db, CLEAN_INDEX_IN_PROGRESS_META_KEY, "");
|
|
12193
|
-
|
|
13791
|
+
completeCoverageIntent(db);
|
|
13792
|
+
return { reconciled: collectRepoFiles(resolved, db) };
|
|
12194
13793
|
}
|
|
12195
13794
|
const reconciled = [...new Set(changes)]
|
|
12196
13795
|
.map((file) => path.relative(resolved, path.resolve(resolved, file)))
|
|
12197
|
-
.filter((file) =>
|
|
13796
|
+
.filter((file) => coverageContains(requireIndexCoverage(coverageForDatabase(db)), file))
|
|
12198
13797
|
.sort(compareBytes);
|
|
12199
13798
|
for (const file of reconciled) {
|
|
12200
13799
|
const absolute = path.join(resolved, file);
|
|
@@ -12233,10 +13832,16 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12233
13832
|
.filter((file) => !reconciledSet.has(file));
|
|
12234
13833
|
for (const file of knownPaths)
|
|
12235
13834
|
recordIndexState(db, resolved, file);
|
|
12236
|
-
await refreshFlowsForApexFiles(db, resolved, reconciled);
|
|
13835
|
+
const refreshed = await refreshFlowsForApexFiles(db, resolved, reconciled);
|
|
13836
|
+
for (const file of refreshed) {
|
|
13837
|
+
if (!reconciledSet.has(file))
|
|
13838
|
+
reconciled.push(file);
|
|
13839
|
+
reconciledSet.add(file);
|
|
13840
|
+
}
|
|
12237
13841
|
persistSymbolIdentities(db, resolved, reconciled);
|
|
12238
13842
|
reconcileTypeScriptDi(db, resolved);
|
|
12239
|
-
|
|
13843
|
+
if (getMeta(db, "semanticDeferred") !== "1")
|
|
13844
|
+
await indexEmbeddings(db, resolved);
|
|
12240
13845
|
setMeta(db, "lastIndexedHead", gitHead(resolved) ?? "");
|
|
12241
13846
|
setMeta(db, "knodinVersion", KNODIN_VERSION);
|
|
12242
13847
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
@@ -12251,6 +13856,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12251
13856
|
const issues = [];
|
|
12252
13857
|
const db = new Database(candidate.databasePath, { readonly: true });
|
|
12253
13858
|
try {
|
|
13859
|
+
const coverage = coverageForDatabase(db);
|
|
13860
|
+
if (coverage.origin === "invalid")
|
|
13861
|
+
issues.push("invalid-index-coverage");
|
|
13862
|
+
if (coverage.intentState === "pending")
|
|
13863
|
+
issues.push("pending-index-coverage");
|
|
12254
13864
|
const quick = db.query("PRAGMA quick_check").all();
|
|
12255
13865
|
if (quick.length !== 1 || Object.values(quick[0] ?? {})[0] !== "ok")
|
|
12256
13866
|
issues.push("sqlite-integrity");
|
|
@@ -12278,11 +13888,22 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12278
13888
|
const symbols = db.query("SELECT COUNT(*) count FROM symbols").get()?.count ?? 0;
|
|
12279
13889
|
const embeddings = db.query("SELECT COUNT(*) count FROM symbol_embeddings").get()
|
|
12280
13890
|
?.count ?? 0;
|
|
12281
|
-
if (symbols !== embeddings)
|
|
13891
|
+
if (symbols !== embeddings && getMeta(db, "semanticDeferred") !== "1")
|
|
12282
13892
|
issues.push("semantic-index-incomplete");
|
|
13893
|
+
if (db
|
|
13894
|
+
.query(`SELECT COUNT(*) count FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`)
|
|
13895
|
+
.get()?.count)
|
|
13896
|
+
issues.push("orphaned-embeddings");
|
|
12283
13897
|
}
|
|
12284
13898
|
if (tables.has("index_state")) {
|
|
12285
|
-
|
|
13899
|
+
if (missingApexConsumerDescriptorFiles(db).length)
|
|
13900
|
+
issues.push("apex-consumer-descriptors");
|
|
13901
|
+
if ((db
|
|
13902
|
+
.query("SELECT COUNT(*) count FROM index_state WHERE lower(filePath) LIKE '%.cls' OR lower(filePath) LIKE '%.trigger'")
|
|
13903
|
+
.get()?.count ?? 0) > 0 &&
|
|
13904
|
+
!apexConsumerMaterializationValid(db))
|
|
13905
|
+
issues.push("apex-consumer-materialization");
|
|
13906
|
+
const sourceFiles = collectRepoFiles(candidate.repo, db);
|
|
12286
13907
|
const sourceSet = new Set(sourceFiles);
|
|
12287
13908
|
const indexed = db
|
|
12288
13909
|
.query("SELECT filePath, mtimeMs, size FROM index_state")
|
|
@@ -12351,6 +13972,9 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12351
13972
|
async explain(symbol, repoPath, detailLevel = "standard", selector = {}) {
|
|
12352
13973
|
const repos = await getFederatedRepos(repoPath);
|
|
12353
13974
|
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
13975
|
+
const indexCoverage = allRepos.length === 1
|
|
13976
|
+
? indexCoverageSummary(coverageForDatabase(allRepos[0].db))
|
|
13977
|
+
: undefined;
|
|
12354
13978
|
let targetRepoPath = repoPath;
|
|
12355
13979
|
let primaryDef = null;
|
|
12356
13980
|
const matches = [];
|
|
@@ -12384,6 +14008,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12384
14008
|
})),
|
|
12385
14009
|
};
|
|
12386
14010
|
return {
|
|
14011
|
+
...(indexCoverage ? { indexCoverage } : {}),
|
|
12387
14012
|
symbol,
|
|
12388
14013
|
summary: ambiguity.message,
|
|
12389
14014
|
source: "",
|
|
@@ -12402,6 +14027,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12402
14027
|
}
|
|
12403
14028
|
if (!primaryDef) {
|
|
12404
14029
|
return {
|
|
14030
|
+
...(indexCoverage ? { indexCoverage } : {}),
|
|
12405
14031
|
symbol,
|
|
12406
14032
|
summary: `Symbol "${symbol}" not found in codebase.`,
|
|
12407
14033
|
source: "",
|
|
@@ -12508,6 +14134,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12508
14134
|
allBlastFiles.length > cap ||
|
|
12509
14135
|
omissions.length > cap;
|
|
12510
14136
|
return {
|
|
14137
|
+
...(indexCoverage ? { indexCoverage } : {}),
|
|
12511
14138
|
identity: symbolIdentity(targetRepoPath, primaryDef),
|
|
12512
14139
|
symbol,
|
|
12513
14140
|
summary,
|
|
@@ -13192,9 +14819,41 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13192
14819
|
const db = activeDb ??
|
|
13193
14820
|
(fs.existsSync(diskDbPath) ? new Database(diskDbPath, { readonly: true }) : null);
|
|
13194
14821
|
const verifiedAt = new Date().toISOString();
|
|
14822
|
+
if (db && coverageForDatabase(db).origin === "invalid") {
|
|
14823
|
+
const result = {
|
|
14824
|
+
status: "repair-needed",
|
|
14825
|
+
repo: resolved,
|
|
14826
|
+
version: KNODIN_VERSION,
|
|
14827
|
+
schemaVersion: 0,
|
|
14828
|
+
model: { id: "Xenova/all-MiniLM-L6-v2", dimensions: 384, locality: "local" },
|
|
14829
|
+
indexCoverage: indexCoverageSummary(coverageForDatabase(db)),
|
|
14830
|
+
coverage: {
|
|
14831
|
+
sourceFiles: 0,
|
|
14832
|
+
indexedFiles: 0,
|
|
14833
|
+
filesWithSymbols: 0,
|
|
14834
|
+
percent: 0,
|
|
14835
|
+
countsUnknown: true,
|
|
14836
|
+
},
|
|
14837
|
+
orphaned: { embeddings: 0, references: 0, dependencies: 0 },
|
|
14838
|
+
missing: {
|
|
14839
|
+
files: [],
|
|
14840
|
+
records: ["malformed index coverage intent; inventory was not enumerated"],
|
|
14841
|
+
},
|
|
14842
|
+
lastSuccessfulReconciliation: null,
|
|
14843
|
+
freshness: buildFreshnessEnvelope(resolved, db, verifiedAt, "repair-needed", null),
|
|
14844
|
+
verification: { mode: "deep-audit", verifiedAt },
|
|
14845
|
+
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
14846
|
+
repairSteps: [
|
|
14847
|
+
"Restore valid coverage metadata, or explicitly rebuild a full candidate with knodin index --clean. Automatic repair cannot guess the intended scope.",
|
|
14848
|
+
],
|
|
14849
|
+
};
|
|
14850
|
+
if (!activeDb)
|
|
14851
|
+
db.close();
|
|
14852
|
+
return result;
|
|
14853
|
+
}
|
|
13195
14854
|
const activity = options.ignoreActiveOperation ? null : readIndexActivity(resolved);
|
|
13196
14855
|
if (activity) {
|
|
13197
|
-
const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
|
|
14856
|
+
const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved, db));
|
|
13198
14857
|
const sourceFiles = collected.files;
|
|
13199
14858
|
const skipped = buildCoverageSkips(collected.skippedByExtension, db);
|
|
13200
14859
|
let schemaVersion = 0;
|
|
@@ -13322,7 +14981,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13322
14981
|
const persisted = readPersistedStatusAudit(resolved, diskDbPath, databaseFingerprint, schemaVersion);
|
|
13323
14982
|
if (persisted) {
|
|
13324
14983
|
throwIfAborted(options.signal);
|
|
13325
|
-
const gitProbe = measurePerfPhaseSync("freshness_probe", () => gitWorkTreeProbe(resolved));
|
|
14984
|
+
const gitProbe = measurePerfPhaseSync("freshness_probe", () => gitWorkTreeProbe(resolved, requireIndexCoverage(coverageForDatabase(db))));
|
|
13326
14985
|
const drift = probeDrift(resolved, db, gitProbe);
|
|
13327
14986
|
const probeVerifiedAt = new Date().toISOString();
|
|
13328
14987
|
const freshness = buildFreshnessEnvelope(resolved, db, probeVerifiedAt, drift.verifiable ? (drift.drifted ? "stale-working-tree" : undefined) : "unknown", gitProbe);
|
|
@@ -13400,8 +15059,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13400
15059
|
}
|
|
13401
15060
|
throwIfAborted(options.signal);
|
|
13402
15061
|
const collected = options.signal
|
|
13403
|
-
? await measurePerfPhase("status_audit", () => collectRepoFilesWithCoverageAbortable(resolved, options.signal))
|
|
13404
|
-
: measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
|
|
15062
|
+
? await measurePerfPhase("status_audit", () => collectRepoFilesWithCoverageAbortable(resolved, options.signal, db))
|
|
15063
|
+
: measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved, db));
|
|
13405
15064
|
throwIfAborted(options.signal);
|
|
13406
15065
|
const sourceFiles = collected.files;
|
|
13407
15066
|
const coverageSkips = buildCoverageSkips(collected.skippedByExtension, db);
|
|
@@ -13498,8 +15157,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13498
15157
|
await yieldToIndexEventLoop();
|
|
13499
15158
|
}
|
|
13500
15159
|
const row = indexedRows[index];
|
|
13501
|
-
const exists = fs.existsSync(path.join(resolved, row.filePath));
|
|
13502
15160
|
if (!sourceFileSet.has(row.filePath)) {
|
|
15161
|
+
const exists = fs.existsSync(path.join(resolved, row.filePath));
|
|
13503
15162
|
(exists ? ineligibleFiles : staleFiles).push(row.filePath);
|
|
13504
15163
|
continue;
|
|
13505
15164
|
}
|
|
@@ -13523,6 +15182,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13523
15182
|
AND NOT (d.fromFile LIKE '.knodin/dbt/%' AND EXISTS (SELECT 1 FROM symbols s WHERE s.filePath=d.fromFile))`),
|
|
13524
15183
|
};
|
|
13525
15184
|
const records = staleFiles.map((file) => `indexed file no longer exists: ${file}`);
|
|
15185
|
+
for (const file of missingApexConsumerDescriptorFiles(db))
|
|
15186
|
+
records.push(`Apex class-consumer descriptor missing or invalid: ${file}`);
|
|
15187
|
+
if (indexedRows.some((row) => /\.(cls|trigger)$/i.test(row.filePath)) &&
|
|
15188
|
+
!apexConsumerMaterializationValid(db))
|
|
15189
|
+
records.push("Apex class-consumer binding materialization is incomplete");
|
|
13526
15190
|
for (const file of ineligibleFiles)
|
|
13527
15191
|
records.push(`indexed file is no longer eligible: ${file}`);
|
|
13528
15192
|
for (const file of damagedFiles)
|
|
@@ -13535,10 +15199,13 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13535
15199
|
records.push(`${orphaned.dependencies} dependency row(s) lack an indexed source file`);
|
|
13536
15200
|
if (getMeta(db, "repairPostprocessingPending") === "1")
|
|
13537
15201
|
records.push("repair post-processing is incomplete");
|
|
15202
|
+
if (coverageForDatabase(db).intentState === "pending")
|
|
15203
|
+
records.push("index coverage operation is pending");
|
|
13538
15204
|
const needsRepair = missingFiles.length > 0 || records.length > 0;
|
|
13539
15205
|
const freshness = buildFreshnessEnvelope(resolved, db, verifiedAt, needsRepair ? "repair-needed" : undefined);
|
|
13540
15206
|
const stale = !needsRepair && freshness.state !== "fresh";
|
|
13541
15207
|
const result = {
|
|
15208
|
+
indexCoverage: indexCoverageSummary(coverageForDatabase(db)),
|
|
13542
15209
|
status: needsRepair ? "repair-needed" : stale ? "stale" : "healthy",
|
|
13543
15210
|
repo: resolved,
|
|
13544
15211
|
version: KNODIN_VERSION,
|
|
@@ -13585,7 +15252,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13585
15252
|
};
|
|
13586
15253
|
if (!needsRepair && !stale)
|
|
13587
15254
|
noteFreshness(resolved, "fresh");
|
|
13588
|
-
if (activeDb) {
|
|
15255
|
+
if (activeDb && options.checkpoint !== "defer") {
|
|
13589
15256
|
try {
|
|
13590
15257
|
db.run("PRAGMA wal_checkpoint(PASSIVE);");
|
|
13591
15258
|
}
|
|
@@ -13726,12 +15393,19 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13726
15393
|
}
|
|
13727
15394
|
emitProgress("planning", 0, "Building the repair plan");
|
|
13728
15395
|
const repairKinds = new Map();
|
|
15396
|
+
const descriptorRepairPaths = new Set();
|
|
13729
15397
|
for (const file of before.missing.files)
|
|
13730
15398
|
repairKinds.set(file, "indexed");
|
|
13731
15399
|
for (const record of before.missing.records) {
|
|
13732
15400
|
const damagedPrefix = "indexed snapshot differs from disk: ";
|
|
13733
15401
|
const stalePrefix = "indexed file no longer exists: ";
|
|
13734
15402
|
const ineligiblePrefix = "indexed file is no longer eligible: ";
|
|
15403
|
+
const descriptorPrefix = "Apex class-consumer descriptor missing or invalid: ";
|
|
15404
|
+
if (record.startsWith(descriptorPrefix)) {
|
|
15405
|
+
const file = record.slice(descriptorPrefix.length);
|
|
15406
|
+
descriptorRepairPaths.add(file);
|
|
15407
|
+
repairKinds.set(file, "damaged");
|
|
15408
|
+
}
|
|
13735
15409
|
if (record.startsWith(damagedPrefix))
|
|
13736
15410
|
repairKinds.set(record.slice(damagedPrefix.length), "damaged");
|
|
13737
15411
|
else if (record.startsWith(stalePrefix))
|
|
@@ -13779,9 +15453,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13779
15453
|
if (schemaPrepared)
|
|
13780
15454
|
committedWork = true;
|
|
13781
15455
|
const inheritedPostprocessing = getMeta(db, "repairPostprocessingPending") === "1";
|
|
13782
|
-
const eligibleRepairPaths = new Set(collectRepoFiles(resolved));
|
|
15456
|
+
const eligibleRepairPaths = new Set(collectRepoFiles(resolved, db));
|
|
13783
15457
|
repairPaths = [...repairKinds.keys()]
|
|
13784
|
-
.filter((file) => repairKinds.get(file) === "stale" ||
|
|
15458
|
+
.filter((file) => repairKinds.get(file) === "stale" ||
|
|
15459
|
+
descriptorRepairPaths.has(file) ||
|
|
15460
|
+
fileDriftedFromIndexState(db, resolved, file))
|
|
13785
15461
|
.sort(compareBytes);
|
|
13786
15462
|
const repairAffectsTypeScriptDi = inheritedPostprocessing || repairPathsAffectTypeScriptDi(db, resolved, repairPaths);
|
|
13787
15463
|
counts.skipped = Math.max(0, repairKinds.size - repairPaths.length);
|
|
@@ -13853,17 +15529,31 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13853
15529
|
});
|
|
13854
15530
|
cancelBoundary();
|
|
13855
15531
|
}
|
|
13856
|
-
const needsPostprocessing = repairPaths.length > 0 ||
|
|
15532
|
+
const needsPostprocessing = repairPaths.length > 0 ||
|
|
15533
|
+
before.missing.records.includes("Apex class-consumer binding materialization is incomplete") ||
|
|
15534
|
+
getMeta(db, "repairPostprocessingPending") === "1" ||
|
|
15535
|
+
coverageForDatabase(db).intentState === "pending";
|
|
13857
15536
|
if (needsPostprocessing) {
|
|
13858
15537
|
emitProgress("symbol-identities", 0, "Persisting stable symbol identities", {
|
|
13859
15538
|
phaseTotal: 1,
|
|
13860
15539
|
});
|
|
13861
15540
|
cancelBoundary();
|
|
13862
|
-
const
|
|
15541
|
+
const bindingPaths = inheritedPostprocessing
|
|
15542
|
+
? db
|
|
15543
|
+
.query("SELECT filePath FROM index_state WHERE filePath LIKE '%.cls' OR filePath LIKE '%.cmp' OR filePath LIKE 'force-app/main/default/lwc/%.js' OR filePath LIKE 'force-app/main/default/aura/%.app' OR filePath LIKE 'force-app/main/default/pages/%.page' UNION SELECT toFile AS filePath FROM dependencies WHERE toFile LIKE '%.cls' OR toFile LIKE '%.cmp' OR toFile LIKE 'force-app/main/default/lwc/%.js' OR toFile LIKE 'force-app/main/default/aura/%.app' OR toFile LIKE 'force-app/main/default/pages/%.page'")
|
|
15544
|
+
.all()
|
|
15545
|
+
.map((row) => row.filePath)
|
|
15546
|
+
: repairPaths;
|
|
15547
|
+
const refreshed = await refreshFlowsForApexFiles(db, resolved, bindingPaths);
|
|
15548
|
+
const identityScope = inheritedPostprocessing
|
|
15549
|
+
? undefined
|
|
15550
|
+
: [...new Set([...repairPaths, ...refreshed])];
|
|
13863
15551
|
if (options?.signal)
|
|
13864
15552
|
await persistSymbolIdentitiesCancellable(db, resolved, identityScope, activeOperation.controller.signal);
|
|
13865
15553
|
else
|
|
13866
15554
|
persistSymbolIdentities(db, resolved, identityScope);
|
|
15555
|
+
if (before.missing.records.includes("Apex class-consumer binding materialization is incomplete"))
|
|
15556
|
+
persistApexClassConsumerEndpoints(db);
|
|
13867
15557
|
if (repairAffectsTypeScriptDi)
|
|
13868
15558
|
reconcileTypeScriptDi(db, resolved);
|
|
13869
15559
|
committedWork = true;
|
|
@@ -13958,11 +15648,12 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
13958
15648
|
// revision; a second deep audit proves the complete envelope.
|
|
13959
15649
|
const contentVerified = after.coverage.sourceFiles === after.coverage.indexedFiles &&
|
|
13960
15650
|
after.missing.files.length === 0 &&
|
|
13961
|
-
after.missing.records.
|
|
15651
|
+
after.missing.records.every((record) => record === "index coverage operation is pending") &&
|
|
13962
15652
|
Object.values(after.orphaned).every((count) => count === 0);
|
|
13963
15653
|
if (contentVerified) {
|
|
13964
15654
|
db.run("BEGIN TRANSACTION;");
|
|
13965
15655
|
try {
|
|
15656
|
+
markCoverageIntentComplete(db);
|
|
13966
15657
|
if (after.freshness.state === "stale-head" || after.freshness.state === "unknown") {
|
|
13967
15658
|
setMeta(db, "lastIndexedHead", gitHead(resolved) ?? "");
|
|
13968
15659
|
}
|
|
@@ -14067,26 +15758,60 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14067
15758
|
},
|
|
14068
15759
|
async index(repoPath, files, clean = false, options) {
|
|
14069
15760
|
const resolved = path.resolve(repoPath);
|
|
14070
|
-
// Expand `--under` before anything else, so everything downstream sees an
|
|
14071
|
-
// ordinary explicit-file index and no path can route a scoped rebuild
|
|
14072
|
-
// into the destructive full-index branch.
|
|
14073
15761
|
const scopedUnder = options?.under;
|
|
14074
|
-
|
|
14075
|
-
const
|
|
14076
|
-
.relative(resolved, path.resolve(resolved,
|
|
15762
|
+
const normalizeSelector = (file, allowRoot = false) => {
|
|
15763
|
+
const relative = path
|
|
15764
|
+
.relative(resolved, path.resolve(resolved, file))
|
|
14077
15765
|
.split(path.sep)
|
|
14078
15766
|
.join("/");
|
|
14079
|
-
if (
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
15767
|
+
if (!(allowRoot && relative === "") &&
|
|
15768
|
+
(!canonicalCoveragePath(relative) || !isIndexablePath(relative)))
|
|
15769
|
+
throw new Error(`knodin index: selector is invalid or outside the repository: ${file}`);
|
|
15770
|
+
if (relative &&
|
|
15771
|
+
fs.existsSync(path.join(resolved, relative)) &&
|
|
15772
|
+
!safeCoveragePath(resolved, relative))
|
|
15773
|
+
throw new Error(`knodin index: selector follows a symbolic link: ${file}`);
|
|
15774
|
+
return relative;
|
|
15775
|
+
};
|
|
15776
|
+
const prefix = scopedUnder === undefined ? undefined : normalizeSelector(scopedUnder, true);
|
|
15777
|
+
files = files?.length
|
|
15778
|
+
? [...new Set(files.map((file) => normalizeSelector(file)))]
|
|
15779
|
+
: undefined;
|
|
15780
|
+
// Manifests are resolution inputs, not source coverage selectors. A
|
|
15781
|
+
// config-only request must never fall through to a full repository index.
|
|
15782
|
+
const packageInputs = (files ?? []).filter((file) => path.posix.basename(file) === "package.json" &&
|
|
15783
|
+
!safeCoveragePath(resolved, file)?.isDirectory());
|
|
15784
|
+
if (packageInputs.length) {
|
|
15785
|
+
const packageInputSet = new Set(packageInputs);
|
|
15786
|
+
files = files?.filter((file) => !packageInputSet.has(file));
|
|
15787
|
+
if (!files?.length && prefix === undefined)
|
|
15788
|
+
throw new Error("knodin index: configuration inputs require source files or --under; no source work requested");
|
|
15789
|
+
}
|
|
15790
|
+
for (const file of files ?? [])
|
|
15791
|
+
if (!isIndexableSourcePath(file) || safeCoveragePath(resolved, file)?.isDirectory())
|
|
15792
|
+
throw new Error(`knodin index: explicit file selector must name an indexable file: ${file}`);
|
|
15793
|
+
const requestedCoverage = prefix === ""
|
|
15794
|
+
? null
|
|
15795
|
+
: prefix !== undefined
|
|
15796
|
+
? { prefixes: [prefix], files: files ?? [] }
|
|
15797
|
+
: files
|
|
15798
|
+
? { prefixes: [], files }
|
|
15799
|
+
: null;
|
|
15800
|
+
let promoteScope = false;
|
|
15801
|
+
if (requestedCoverage === null && !openPolicy.databasePath) {
|
|
15802
|
+
const active = dbInstances.get(resolved);
|
|
15803
|
+
const disk = resolveDbPath(resolved);
|
|
15804
|
+
const prior = active ?? (fs.existsSync(disk) ? new Database(disk, { readonly: true }) : null);
|
|
15805
|
+
try {
|
|
15806
|
+
if (prior) {
|
|
15807
|
+
const state = coverageForDatabase(prior);
|
|
15808
|
+
promoteScope = state.intent?.mode === "scoped" || state.origin === "invalid";
|
|
15809
|
+
}
|
|
15810
|
+
}
|
|
15811
|
+
finally {
|
|
15812
|
+
if (prior && prior !== active)
|
|
15813
|
+
prior.close();
|
|
15814
|
+
}
|
|
14090
15815
|
}
|
|
14091
15816
|
const indexCoordination = acquireLifecycleCoordination({
|
|
14092
15817
|
command: "index",
|
|
@@ -14094,8 +15819,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14094
15819
|
verificationToken: process.env.KNODIN_UPDATE_VERIFICATION_TOKEN,
|
|
14095
15820
|
});
|
|
14096
15821
|
try {
|
|
14097
|
-
if (
|
|
14098
|
-
(
|
|
15822
|
+
if (requestedCoverage === null &&
|
|
15823
|
+
(clean || promoteScope) &&
|
|
14099
15824
|
!openPolicy.databasePath &&
|
|
14100
15825
|
!process.env.VITEST &&
|
|
14101
15826
|
process.env.NODE_ENV !== "test") {
|
|
@@ -14153,6 +15878,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14153
15878
|
return {
|
|
14154
15879
|
indexed: reconciliation.reconciled,
|
|
14155
15880
|
unchanged: [],
|
|
15881
|
+
semanticReadiness: health.semanticReadiness,
|
|
15882
|
+
indexCoverage: health.indexCoverage,
|
|
14156
15883
|
verification: {
|
|
14157
15884
|
status: health.status,
|
|
14158
15885
|
issueCount,
|
|
@@ -14192,6 +15919,28 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14192
15919
|
skipContentReconciliation: true,
|
|
14193
15920
|
skipFreshnessGuard: true,
|
|
14194
15921
|
});
|
|
15922
|
+
const existingCoverage = coverageForDatabase(db);
|
|
15923
|
+
const freshCoverage = existingCoverage.origin === "legacy" &&
|
|
15924
|
+
!getMeta(db, "lastSuccessfulReconciliation") &&
|
|
15925
|
+
(db.query("SELECT COUNT(*) count FROM index_state").get()
|
|
15926
|
+
?.count ?? 0) === 0;
|
|
15927
|
+
const intent = mergeIndexCoverage(existingCoverage, requestedCoverage, freshCoverage);
|
|
15928
|
+
writeCoverageIntent(db, resolved, intent);
|
|
15929
|
+
// Persist the requested semantic policy even if collection fails or
|
|
15930
|
+
// the prefix is currently empty; recovery must not start inference.
|
|
15931
|
+
setMeta(db, "semanticDeferred", options?.skipEmbeddings === true ? "1" : "0");
|
|
15932
|
+
if (prefix !== undefined && prefix !== "") {
|
|
15933
|
+
const selection = {
|
|
15934
|
+
version: 1,
|
|
15935
|
+
mode: "scoped",
|
|
15936
|
+
revision: intent.revision,
|
|
15937
|
+
prefixes: [prefix],
|
|
15938
|
+
files: files ?? [],
|
|
15939
|
+
};
|
|
15940
|
+
files = collectRepoFilesWithCoverage(resolved, db, selection).files;
|
|
15941
|
+
if (files.length === 0)
|
|
15942
|
+
throw new Error(`knodin index --under: no indexable files under ${scopedUnder}`);
|
|
15943
|
+
}
|
|
14195
15944
|
let completionMessage = "Local graph is current";
|
|
14196
15945
|
if (files && files.length > 0) {
|
|
14197
15946
|
progress("indexing-files", 0, `Checking ${files.length.toLocaleString()} requested files`, {
|
|
@@ -14205,7 +15954,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14205
15954
|
unchanged.push(relativePath);
|
|
14206
15955
|
}
|
|
14207
15956
|
else if (fs.existsSync(absPath)) {
|
|
14208
|
-
await indexFile(absPath, relativePath, repoPath, db);
|
|
15957
|
+
await explicitPackageInputs.run(packageInputs, () => indexFile(absPath, relativePath, repoPath, db));
|
|
14209
15958
|
recordIndexState(db, repoPath, relativePath);
|
|
14210
15959
|
}
|
|
14211
15960
|
else if (shouldPurgeMissingFile(db)) {
|
|
@@ -14236,9 +15985,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14236
15985
|
progress("finalizing", 0, "Refreshing identities and semantic embeddings");
|
|
14237
15986
|
persistSymbolIdentities(db, repoPath, indexed);
|
|
14238
15987
|
reconcileTypeScriptDi(db, repoPath);
|
|
14239
|
-
await indexEmbeddings(db, repoPath, progress);
|
|
14240
15988
|
indexGeneration++;
|
|
14241
15989
|
}
|
|
15990
|
+
if (!options?.skipEmbeddings)
|
|
15991
|
+
await indexEmbeddings(db, repoPath, progress);
|
|
14242
15992
|
const alreadyCurrent = unchanged.length === 1
|
|
14243
15993
|
? "1 requested file already current"
|
|
14244
15994
|
: `${unchanged.length.toLocaleString()} requested files already current`;
|
|
@@ -14336,10 +16086,13 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14336
16086
|
});
|
|
14337
16087
|
const contentVerified = health.coverage.sourceFiles === health.coverage.indexedFiles &&
|
|
14338
16088
|
health.missing.files.length === 0 &&
|
|
14339
|
-
health.missing.records.
|
|
16089
|
+
health.missing.records.every((record) => record === "index coverage operation is pending") &&
|
|
14340
16090
|
Object.values(health.orphaned).every((count) => count === 0);
|
|
14341
16091
|
if (contentVerified &&
|
|
14342
|
-
(
|
|
16092
|
+
(coverageForDatabase(db).intentState === "pending" ||
|
|
16093
|
+
health.freshness.state === "stale-head" ||
|
|
16094
|
+
health.freshness.state === "unknown")) {
|
|
16095
|
+
completeCoverageIntent(db);
|
|
14343
16096
|
setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
|
|
14344
16097
|
setMeta(db, "knodinVersion", KNODIN_VERSION);
|
|
14345
16098
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
@@ -14394,6 +16147,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14394
16147
|
return {
|
|
14395
16148
|
indexed,
|
|
14396
16149
|
unchanged,
|
|
16150
|
+
indexCoverage: indexCoverageSummary(coverageForDatabase(db)),
|
|
14397
16151
|
// Measured from the graph rather than inferred from the flag: a
|
|
14398
16152
|
// run that deferred embeddings and a run that never had any are
|
|
14399
16153
|
// the same state to a consumer, and a partially embedded graph is
|
|
@@ -14488,6 +16242,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14488
16242
|
};
|
|
14489
16243
|
const formatPath = makePathFormatter(resolvedRepoPath);
|
|
14490
16244
|
const retrievalMode = options.retrievalMode ?? "complete";
|
|
16245
|
+
const hasSearchVectors = [...searchSnapshots.values()].some((snapshot) => snapshot.rows.some((row) => row.vector !== null && accepts(row)));
|
|
14491
16246
|
const normalize = (value) => value.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
|
14492
16247
|
const hydrateStructural = (matches) => {
|
|
14493
16248
|
const fileToCommunityMap = matches.length > 0
|
|
@@ -14733,7 +16488,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14733
16488
|
[...lexicalRanks.values()].some((ranks) => ranks.and.length > 0);
|
|
14734
16489
|
if (retrievalMode === "lexical-only" ||
|
|
14735
16490
|
retrievalMode === "bounded-graph" ||
|
|
14736
|
-
structurallySufficient
|
|
16491
|
+
structurallySufficient ||
|
|
16492
|
+
!hasSearchVectors) {
|
|
14737
16493
|
const matches = allRepos.flatMap((repo) => {
|
|
14738
16494
|
const snapshot = searchSnapshots.get(repo.path);
|
|
14739
16495
|
const ranks = lexicalRanks.get(repo.path);
|
|
@@ -14764,6 +16520,19 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14764
16520
|
};
|
|
14765
16521
|
}
|
|
14766
16522
|
}
|
|
16523
|
+
// No query vector can retrieve candidates from an empty vector corpus.
|
|
16524
|
+
// Explicit vector-only requests must not trigger model loading either.
|
|
16525
|
+
if (!hasSearchVectors) {
|
|
16526
|
+
return {
|
|
16527
|
+
results: [],
|
|
16528
|
+
total: 0,
|
|
16529
|
+
offset,
|
|
16530
|
+
limit: maxResults,
|
|
16531
|
+
hasMore: false,
|
|
16532
|
+
retrieval: { route: [], embeddingsUsed: false },
|
|
16533
|
+
semanticReadiness,
|
|
16534
|
+
};
|
|
16535
|
+
}
|
|
14767
16536
|
// 1. Generate query embedding once
|
|
14768
16537
|
const queryVec = await generateMeasuredEmbedding(query, true);
|
|
14769
16538
|
const rankedCandidates = [];
|
|
@@ -14988,7 +16757,59 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
14988
16757
|
semanticReadiness,
|
|
14989
16758
|
};
|
|
14990
16759
|
},
|
|
16760
|
+
async queryClassConsumers(repoPath, request) {
|
|
16761
|
+
const resolved = path.resolve(repoPath);
|
|
16762
|
+
const db = await openDb(resolved);
|
|
16763
|
+
return readClassConsumerPage(db, resolved, request);
|
|
16764
|
+
},
|
|
14991
16765
|
async query(pattern, target, repoPath, to, limit, depth, detailLevel, selector = {}, impactOptions = {}, options = {}) {
|
|
16766
|
+
if (pattern === "class_consumers") {
|
|
16767
|
+
if (to !== undefined ||
|
|
16768
|
+
depth !== undefined ||
|
|
16769
|
+
options.kinds ||
|
|
16770
|
+
options.direction ||
|
|
16771
|
+
options.relationKinds ||
|
|
16772
|
+
options.sort ||
|
|
16773
|
+
options.topN ||
|
|
16774
|
+
options.base ||
|
|
16775
|
+
options.minLines !== undefined ||
|
|
16776
|
+
options.minComplexity !== undefined)
|
|
16777
|
+
throw new Error("class_consumers supports only target selectors, testScope/path, limit and typed continuation budgets");
|
|
16778
|
+
const secret = evidenceHandleSecret(path.resolve(repoPath)).secret;
|
|
16779
|
+
const cursor = options.classConsumerContinuation
|
|
16780
|
+
? readClassConsumerCursor(options.classConsumerContinuation, secret)
|
|
16781
|
+
: undefined;
|
|
16782
|
+
const request = {
|
|
16783
|
+
symbol: target,
|
|
16784
|
+
selector,
|
|
16785
|
+
testScope: options.testScope ?? "all",
|
|
16786
|
+
path: options.path,
|
|
16787
|
+
limit: limit ?? 100,
|
|
16788
|
+
...(cursor
|
|
16789
|
+
? {
|
|
16790
|
+
after: cursor.after,
|
|
16791
|
+
expectedQueryDigest: cursor.queryDigest,
|
|
16792
|
+
expectedSnapshotDigest: cursor.snapshotDigest,
|
|
16793
|
+
}
|
|
16794
|
+
: {}),
|
|
16795
|
+
};
|
|
16796
|
+
const page = await this.queryClassConsumers(repoPath, request);
|
|
16797
|
+
const envelope = finalizeClassConsumerPage(page, {
|
|
16798
|
+
secret,
|
|
16799
|
+
budget: {
|
|
16800
|
+
bytes: options.classConsumerMaxBytes,
|
|
16801
|
+
tokens: options.classConsumerMaxTokens,
|
|
16802
|
+
items: options.classConsumerMaxItems,
|
|
16803
|
+
},
|
|
16804
|
+
decorate: (candidate) => ({
|
|
16805
|
+
...candidate,
|
|
16806
|
+
target,
|
|
16807
|
+
targetResolution: "resolved",
|
|
16808
|
+
staleness: stalenessFor(repoPath),
|
|
16809
|
+
}),
|
|
16810
|
+
});
|
|
16811
|
+
return { ...envelope, pattern: "class_consumers", target, targetResolution: "resolved" };
|
|
16812
|
+
}
|
|
14992
16813
|
if (pattern === "lsp_diagnostics" ||
|
|
14993
16814
|
pattern === "lsp_definitions" ||
|
|
14994
16815
|
pattern === "lsp_declarations" ||
|
|
@@ -15091,6 +16912,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15091
16912
|
const resolved = resolveSymbolRows(db, primary.path, target, selector);
|
|
15092
16913
|
if (resolved.ambiguity)
|
|
15093
16914
|
return {
|
|
16915
|
+
indexCoverage: indexCoverageSummary(coverageForDatabase(db)),
|
|
15094
16916
|
pattern,
|
|
15095
16917
|
target,
|
|
15096
16918
|
...(to ? { to } : {}),
|
|
@@ -15139,7 +16961,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15139
16961
|
}
|
|
15140
16962
|
const finish = (rows, extra = {}) => {
|
|
15141
16963
|
for (const row of rows) {
|
|
15142
|
-
if (row.identity || !row.symbol || !row.file)
|
|
16964
|
+
if (row.identity || row.callerOrigin || !row.symbol || !row.file)
|
|
15143
16965
|
continue;
|
|
15144
16966
|
const definition = db
|
|
15145
16967
|
.query("SELECT * FROM symbols WHERE name = ? AND filePath = ? LIMIT 1")
|
|
@@ -15148,6 +16970,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15148
16970
|
row.identity = symbolIdentity(primary.path, definition);
|
|
15149
16971
|
}
|
|
15150
16972
|
return {
|
|
16973
|
+
indexCoverage: indexCoverageSummary(coverageForDatabase(db)),
|
|
15151
16974
|
pattern,
|
|
15152
16975
|
target,
|
|
15153
16976
|
...(to ? { to } : {}),
|
|
@@ -15155,6 +16978,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15155
16978
|
results: rows.slice(0, cap),
|
|
15156
16979
|
...(rows.length > cap ? { hasMore: true } : {}),
|
|
15157
16980
|
...(targetResolution ? { targetResolution } : {}),
|
|
16981
|
+
...((pattern === "callers_of" || pattern === "callees_of") &&
|
|
16982
|
+
selectedTarget &&
|
|
16983
|
+
/\.(?:cls|trigger)$/i.test(selectedTarget.filePath)
|
|
16984
|
+
? { apexReceiverCapability: apexReceiverCapability(db) }
|
|
16985
|
+
: {}),
|
|
15158
16986
|
...extra,
|
|
15159
16987
|
};
|
|
15160
16988
|
};
|
|
@@ -15335,7 +17163,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15335
17163
|
}
|
|
15336
17164
|
}
|
|
15337
17165
|
const refs = pattern === "callers_of"
|
|
15338
|
-
? findCallersFederated(allRepos, defRepo, targetName, 1, new Set(), defFile, selectedTarget?.identity ?? null)
|
|
17166
|
+
? findCallersFederated(allRepos, defRepo, targetName, 1, new Set(), defFile, selectedTarget?.identity ?? null, true)
|
|
15339
17167
|
: findCalleesFederated(allRepos, defRepo, targetName, 1, new Set(), defFile, selectedTarget?.identity ?? null);
|
|
15340
17168
|
// One row per caller symbol, but carrying every call site it has.
|
|
15341
17169
|
// Collapsing to the first line lost the rest and made `count`
|
|
@@ -15343,25 +17171,44 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15343
17171
|
const byCaller = new Map();
|
|
15344
17172
|
const rows = [];
|
|
15345
17173
|
let callSiteCount = 0;
|
|
17174
|
+
let bindingSiteCount = 0;
|
|
15346
17175
|
for (const r of refs) {
|
|
15347
17176
|
const file = formatPath(r.repoPath || defRepo, r.filePath);
|
|
15348
|
-
const
|
|
15349
|
-
|
|
17177
|
+
const binding = r.kind === "apex_method";
|
|
17178
|
+
const key = `${r.symbol}|${file}|${binding ? "binding" : "call"}`;
|
|
17179
|
+
if (binding)
|
|
17180
|
+
bindingSiteCount++;
|
|
17181
|
+
else
|
|
17182
|
+
callSiteCount++;
|
|
15350
17183
|
const existing = byCaller.get(key);
|
|
15351
17184
|
if (existing) {
|
|
15352
|
-
|
|
15353
|
-
|
|
17185
|
+
const lines = binding ? existing.bindingSiteLines : existing.callSiteLines;
|
|
17186
|
+
if (r.lineNumber !== undefined && !lines?.includes(r.lineNumber))
|
|
17187
|
+
lines?.push(r.lineNumber);
|
|
15354
17188
|
continue;
|
|
15355
17189
|
}
|
|
15356
|
-
const rr =
|
|
15357
|
-
|
|
15358
|
-
|
|
17190
|
+
const rr = r.callerOrigin
|
|
17191
|
+
? undefined
|
|
17192
|
+
: (r.repoPath ? allRepos.find((x) => x.path === r.repoPath) : primary)?.db
|
|
17193
|
+
.query("SELECT * FROM symbols WHERE name = ? AND filePath = ? LIMIT 1")
|
|
17194
|
+
.get(r.symbol, r.filePath);
|
|
15359
17195
|
const row = {
|
|
15360
17196
|
symbol: r.symbol,
|
|
17197
|
+
...(r.callerOrigin
|
|
17198
|
+
? {
|
|
17199
|
+
callerOrigin: r.callerOrigin,
|
|
17200
|
+
sourceEvidence: "Resolved call site; no named enclosing caller was extracted. Anonymous nesting is not established.",
|
|
17201
|
+
}
|
|
17202
|
+
: {}),
|
|
15361
17203
|
file,
|
|
15362
17204
|
line: r.lineNumber,
|
|
15363
17205
|
kind: r.kind ?? "call",
|
|
15364
|
-
|
|
17206
|
+
...(binding
|
|
17207
|
+
? {
|
|
17208
|
+
bindingSiteLines: r.lineNumber !== undefined ? [r.lineNumber] : [],
|
|
17209
|
+
sourceEvidence: "Static Apex import binding; not evidence that the method executes.",
|
|
17210
|
+
}
|
|
17211
|
+
: { callSiteLines: r.lineNumber !== undefined ? [r.lineNumber] : [] }),
|
|
15365
17212
|
...(rr ? { identity: symbolIdentity(r.repoPath || defRepo, rr) } : {}),
|
|
15366
17213
|
};
|
|
15367
17214
|
byCaller.set(key, row);
|
|
@@ -15369,9 +17216,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15369
17216
|
}
|
|
15370
17217
|
for (const row of rows) {
|
|
15371
17218
|
row.callSiteLines?.sort((a, b) => a - b);
|
|
15372
|
-
row.
|
|
17219
|
+
row.bindingSiteLines?.sort((a, b) => a - b);
|
|
17220
|
+
row.line = row.callSiteLines?.[0] ?? row.bindingSiteLines?.[0] ?? row.line;
|
|
15373
17221
|
}
|
|
15374
|
-
return finish(rows, { callSiteCount });
|
|
17222
|
+
return finish(rows, { callSiteCount, ...(bindingSiteCount ? { bindingSiteCount } : {}) });
|
|
15375
17223
|
}
|
|
15376
17224
|
case "imports_of": {
|
|
15377
17225
|
const stmt = db.query("SELECT DISTINCT toFile, kind FROM dependencies WHERE fromFile = ?");
|
|
@@ -15456,16 +17304,188 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15456
17304
|
});
|
|
15457
17305
|
}
|
|
15458
17306
|
case "inheritors_of": {
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
|
|
15462
|
-
|
|
15463
|
-
file
|
|
15464
|
-
|
|
15465
|
-
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
17307
|
+
db.run("SAVEPOINT apex_heritage_read");
|
|
17308
|
+
try {
|
|
17309
|
+
// Unselected heritage queries historically aggregate names across languages.
|
|
17310
|
+
// An Apex spelling match must not suppress those independent legacy rows.
|
|
17311
|
+
const aggregate = !selector.file && !selector.identity;
|
|
17312
|
+
const nonApexRows = db
|
|
17313
|
+
.query(`SELECT DISTINCT callerSymbol, callerFile, kind, line FROM "references"
|
|
17314
|
+
WHERE calleeSymbol = ? AND kind IN ('extends', 'implements')
|
|
17315
|
+
AND callerSymbol IS NOT NULL AND lower(callerFile) NOT LIKE '%.cls'
|
|
17316
|
+
AND lower(callerFile) NOT LIKE '%.trigger'
|
|
17317
|
+
ORDER BY callerFile COLLATE BINARY, line, kind COLLATE BINARY, callerSymbol COLLATE BINARY LIMIT ?`)
|
|
17318
|
+
.all(selectedTarget?.name ?? target, cap + 1)
|
|
17319
|
+
.map((row) => ({
|
|
17320
|
+
symbol: row.callerSymbol,
|
|
17321
|
+
file: row.callerFile,
|
|
17322
|
+
kind: row.kind,
|
|
17323
|
+
line: row.line,
|
|
17324
|
+
}));
|
|
17325
|
+
const finishHeritage = (apexRows, apex, partial, extra = {}) => {
|
|
17326
|
+
const includeNonApex = aggregate || apex === "not-selected";
|
|
17327
|
+
const merged = [...(includeNonApex ? nonApexRows : []), ...apexRows];
|
|
17328
|
+
const unique = [
|
|
17329
|
+
...new Map(merged.map((row) => [
|
|
17330
|
+
JSON.stringify([row.file, row.line, row.column ?? null, row.kind, row.symbol]),
|
|
17331
|
+
row,
|
|
17332
|
+
])).values(),
|
|
17333
|
+
];
|
|
17334
|
+
unique.sort((a, b) => Buffer.compare(Buffer.from(a.file ?? ""), Buffer.from(b.file ?? "")) ||
|
|
17335
|
+
(a.line ?? 0) - (b.line ?? 0) ||
|
|
17336
|
+
(a.column ?? 0) - (b.column ?? 0) ||
|
|
17337
|
+
Buffer.compare(Buffer.from(a.kind ?? ""), Buffer.from(b.kind ?? "")) ||
|
|
17338
|
+
Buffer.compare(Buffer.from(a.symbol ?? ""), Buffer.from(b.symbol ?? "")));
|
|
17339
|
+
const truncated = unique.length > cap || extra.truncated === true;
|
|
17340
|
+
return finish(unique, {
|
|
17341
|
+
...extra,
|
|
17342
|
+
inheritanceQuery: {
|
|
17343
|
+
version: 1,
|
|
17344
|
+
mode: aggregate || apex === "not-selected" ? "name-aggregate" : "selected-target",
|
|
17345
|
+
partial: partial || truncated,
|
|
17346
|
+
truncated,
|
|
17347
|
+
apex,
|
|
17348
|
+
},
|
|
17349
|
+
...(truncated ? { truncated: true, hasMore: true } : {}),
|
|
17350
|
+
});
|
|
17351
|
+
};
|
|
17352
|
+
const apexCandidates = db
|
|
17353
|
+
.query("SELECT * FROM symbols WHERE kind IN ('class','interface')")
|
|
17354
|
+
.all()
|
|
17355
|
+
.filter((row) => /\.(?:cls|trigger)$/i.test(row.filePath) &&
|
|
17356
|
+
(!selector.file || selector.file === row.filePath) &&
|
|
17357
|
+
(!selector.identity ||
|
|
17358
|
+
symbolIdentityMatches(row.identity ?? "", selector.identity)) &&
|
|
17359
|
+
(!selector.kind || selector.kind === row.kind) &&
|
|
17360
|
+
row.name.toLowerCase() === target.split(".").at(-1)?.toLowerCase());
|
|
17361
|
+
const hasApexInventory = aggregate &&
|
|
17362
|
+
db
|
|
17363
|
+
.query("SELECT 1 AS present FROM index_state WHERE lower(filePath) LIKE '%.cls' OR lower(filePath) LIKE '%.trigger' LIMIT 1")
|
|
17364
|
+
.get() != null;
|
|
17365
|
+
if (apexCandidates.length ||
|
|
17366
|
+
selector.file?.match(/\.(?:cls|trigger)$/i) ||
|
|
17367
|
+
hasApexInventory) {
|
|
17368
|
+
try {
|
|
17369
|
+
const targetPage = readClassConsumerPage(db, repoPath, {
|
|
17370
|
+
symbol: target,
|
|
17371
|
+
selector,
|
|
17372
|
+
testScope: "all",
|
|
17373
|
+
limit: 1,
|
|
17374
|
+
});
|
|
17375
|
+
const symbols = db
|
|
17376
|
+
.query("SELECT * FROM symbols WHERE kind IN ('class','interface') AND identity IS NOT NULL")
|
|
17377
|
+
.all();
|
|
17378
|
+
const selected = symbols.find((row) => row.identity === targetPage.targetIdentity);
|
|
17379
|
+
if (!selected)
|
|
17380
|
+
throw new Error("Apex heritage target changed within the read lease");
|
|
17381
|
+
const stored = db
|
|
17382
|
+
.query("SELECT f.* FROM apex_consumer_files f JOIN index_state i ON i.filePath=f.filePath ORDER BY f.filePath COLLATE BINARY")
|
|
17383
|
+
.all();
|
|
17384
|
+
const facts = stored
|
|
17385
|
+
.map(readApexConsumerFacts)
|
|
17386
|
+
.filter((file) => file !== null);
|
|
17387
|
+
const catalog = createApexConsumerCatalog(facts);
|
|
17388
|
+
const cache = new Map(facts.map((file) => [file.file, file]));
|
|
17389
|
+
const targetOwner = apexConsumerSymbolOwner(db, selected, cache);
|
|
17390
|
+
const rows = [];
|
|
17391
|
+
let work = 0;
|
|
17392
|
+
let partial = facts.length !== stored.length || targetPage.analysis.missingDescriptorFiles > 0;
|
|
17393
|
+
for (const file of facts) {
|
|
17394
|
+
if (file.heritageComplete !== true)
|
|
17395
|
+
partial = true;
|
|
17396
|
+
if (file.parseState !== "complete" || file.heritage === undefined) {
|
|
17397
|
+
partial = true;
|
|
17398
|
+
continue;
|
|
17399
|
+
}
|
|
17400
|
+
for (const edge of file.heritage) {
|
|
17401
|
+
if (++work > 50000 || rows.length >= cap + 1) {
|
|
17402
|
+
partial = true;
|
|
17403
|
+
break;
|
|
17404
|
+
}
|
|
17405
|
+
const site = {
|
|
17406
|
+
version: 1,
|
|
17407
|
+
kind: "variable_type",
|
|
17408
|
+
qualifiedSpelling: edge.typePath,
|
|
17409
|
+
typePath: edge.typePath,
|
|
17410
|
+
memberName: null,
|
|
17411
|
+
lexicalOwner: edge.ownerPath,
|
|
17412
|
+
start: edge.start,
|
|
17413
|
+
end: edge.end,
|
|
17414
|
+
blockedReason: null,
|
|
17415
|
+
};
|
|
17416
|
+
const proof = resolveApexClassUses(catalog, { ...file, sites: [site] })[0];
|
|
17417
|
+
if (proof.status !== "resolved") {
|
|
17418
|
+
partial = true;
|
|
17419
|
+
continue;
|
|
17420
|
+
}
|
|
17421
|
+
if (proof.target?.file !== selected.filePath ||
|
|
17422
|
+
proof.target.declaration.ownerPath !== targetOwner)
|
|
17423
|
+
continue;
|
|
17424
|
+
const owners = symbols.filter((row) => row.filePath === file.file &&
|
|
17425
|
+
apexConsumerSymbolOwner(db, row, cache) === edge.ownerPath);
|
|
17426
|
+
if (owners.length !== 1) {
|
|
17427
|
+
partial = true;
|
|
17428
|
+
continue;
|
|
17429
|
+
}
|
|
17430
|
+
const sourceKind = owners[0].kind;
|
|
17431
|
+
const targetKind = proof.target.declaration.kind;
|
|
17432
|
+
if (!(edge.kind === "implements"
|
|
17433
|
+
? sourceKind === "class" && targetKind === "interface"
|
|
17434
|
+
: (sourceKind === "class" && targetKind === "class") ||
|
|
17435
|
+
(sourceKind === "interface" && targetKind === "interface"))) {
|
|
17436
|
+
partial = true;
|
|
17437
|
+
continue;
|
|
17438
|
+
}
|
|
17439
|
+
rows.push({
|
|
17440
|
+
symbol: owners[0].name,
|
|
17441
|
+
identity: owners[0].identity ?? undefined,
|
|
17442
|
+
file: file.file,
|
|
17443
|
+
kind: edge.kind,
|
|
17444
|
+
line: edge.start.line,
|
|
17445
|
+
column: edge.start.column,
|
|
17446
|
+
heritageEvidence: {
|
|
17447
|
+
spelling: edge.typePath,
|
|
17448
|
+
targetIdentity: targetPage.targetIdentity,
|
|
17449
|
+
grounding: "syntax-resolved",
|
|
17450
|
+
},
|
|
17451
|
+
});
|
|
17452
|
+
}
|
|
17453
|
+
if (work > 50000 || rows.length >= cap + 1)
|
|
17454
|
+
break;
|
|
17455
|
+
}
|
|
17456
|
+
return finishHeritage(rows, "resolved", partial, {
|
|
17457
|
+
...(work > 50000 ? { truncated: true } : {}),
|
|
17458
|
+
...(!aggregate ? { targetResolution: "resolved" } : {}),
|
|
17459
|
+
apexHeritageCapability: {
|
|
17460
|
+
version: 1,
|
|
17461
|
+
partial,
|
|
17462
|
+
limitations: [
|
|
17463
|
+
"Explicit source heritage resolved to unique indexed Apex type identities; not calls or runtime dispatch.",
|
|
17464
|
+
"Malformed, legacy, ambiguous or unsupported heritage is withheld; scope is the local indexed inventory.",
|
|
17465
|
+
],
|
|
17466
|
+
},
|
|
17467
|
+
});
|
|
17468
|
+
}
|
|
17469
|
+
catch (error) {
|
|
17470
|
+
if (!aggregate || !(error instanceof ClassConsumerQueryError))
|
|
17471
|
+
throw error;
|
|
17472
|
+
const state = error.code === "CLASS_CONSUMER_TARGET_AMBIGUOUS"
|
|
17473
|
+
? "ambiguous"
|
|
17474
|
+
: error.code === "CLASS_CONSUMER_TARGET_CAPABILITY_MISSING"
|
|
17475
|
+
? "capability-missing"
|
|
17476
|
+
: error.code === "CLASS_CONSUMER_TARGET_NOT_FOUND"
|
|
17477
|
+
? "not-found"
|
|
17478
|
+
: null;
|
|
17479
|
+
if (!state)
|
|
17480
|
+
throw error;
|
|
17481
|
+
return finishHeritage([], state, true);
|
|
17482
|
+
}
|
|
17483
|
+
}
|
|
17484
|
+
return finishHeritage([], "not-selected", false);
|
|
17485
|
+
}
|
|
17486
|
+
finally {
|
|
17487
|
+
db.run("RELEASE apex_heritage_read");
|
|
17488
|
+
}
|
|
15469
17489
|
}
|
|
15470
17490
|
case "structural_implementations_of": {
|
|
15471
17491
|
const stmt = db.query("SELECT DISTINCT callerSymbol, callerFile, kind, line FROM \"references\" WHERE calleeSymbol = ? AND calleeFile = ? AND kind = 'structural_implements' AND callerSymbol IS NOT NULL");
|
|
@@ -15710,49 +17730,229 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
15710
17730
|
// Explicit compatibility mode for PR/change-set impact. It is intentionally
|
|
15711
17731
|
// labeled file-level and remains an upstream blast-radius query.
|
|
15712
17732
|
if (mode === "file") {
|
|
15713
|
-
const
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
const
|
|
15718
|
-
|
|
15719
|
-
|
|
15720
|
-
|
|
15721
|
-
|
|
15722
|
-
|
|
15723
|
-
|
|
17733
|
+
const maxFiles = 256;
|
|
17734
|
+
const maxEdges = 4096;
|
|
17735
|
+
const maxSeeds = 32;
|
|
17736
|
+
const reasons = new Set();
|
|
17737
|
+
const omissions = new Set();
|
|
17738
|
+
const requested = [
|
|
17739
|
+
...new Set(target
|
|
17740
|
+
.split(",")
|
|
17741
|
+
.map((file) => file.trim())
|
|
17742
|
+
.filter(Boolean)),
|
|
17743
|
+
];
|
|
17744
|
+
if (requested.length > maxSeeds)
|
|
17745
|
+
reasons.add("seed-limit");
|
|
17746
|
+
const validFiles = new Map();
|
|
17747
|
+
const indexedFile = db.query("SELECT 1 AS present FROM index_state WHERE filePath = ?");
|
|
17748
|
+
const sealed = sealedRepositories.get(resolvedRepoPath);
|
|
17749
|
+
const canonicalFile = (file) => {
|
|
17750
|
+
const cached = validFiles.get(file);
|
|
17751
|
+
if (cached !== undefined)
|
|
17752
|
+
return cached;
|
|
17753
|
+
let valid = false;
|
|
17754
|
+
try {
|
|
17755
|
+
if (file &&
|
|
17756
|
+
file.length <= 1024 &&
|
|
17757
|
+
!file.includes("\\") &&
|
|
17758
|
+
!file.includes("\0") &&
|
|
17759
|
+
!path.posix.isAbsolute(file) &&
|
|
17760
|
+
!/^[A-Za-z]:/.test(file) &&
|
|
17761
|
+
path.posix.normalize(file) === file &&
|
|
17762
|
+
!file.split("/").includes("..") &&
|
|
17763
|
+
!file.startsWith(".knodin/") &&
|
|
17764
|
+
isIndexableSourcePath(file) &&
|
|
17765
|
+
indexedFile.get(file)) {
|
|
17766
|
+
if (sealed) {
|
|
17767
|
+
valid = !!sealed
|
|
17768
|
+
.query("SELECT 1 AS present FROM sealed_file WHERE filePath = ?")
|
|
17769
|
+
.get(file);
|
|
17770
|
+
}
|
|
17771
|
+
else {
|
|
17772
|
+
let current = resolvedRepoPath;
|
|
17773
|
+
valid = true;
|
|
17774
|
+
for (const part of file.split("/")) {
|
|
17775
|
+
current = path.join(current, part);
|
|
17776
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
17777
|
+
valid = false;
|
|
17778
|
+
break;
|
|
17779
|
+
}
|
|
17780
|
+
}
|
|
17781
|
+
if (valid)
|
|
17782
|
+
valid = fs.lstatSync(current).isFile();
|
|
17783
|
+
}
|
|
17784
|
+
}
|
|
17785
|
+
}
|
|
17786
|
+
catch {
|
|
17787
|
+
valid = false;
|
|
17788
|
+
}
|
|
17789
|
+
validFiles.set(file, valid);
|
|
17790
|
+
return valid;
|
|
17791
|
+
};
|
|
17792
|
+
const files = requested.slice(0, maxSeeds).filter((file) => {
|
|
17793
|
+
if (canonicalFile(file))
|
|
17794
|
+
return true;
|
|
17795
|
+
omissions.add("A requested path is not an exact indexed regular source file; no basename or symbol-name fallback was used.");
|
|
17796
|
+
return false;
|
|
17797
|
+
});
|
|
17798
|
+
const seeds = new Set(files);
|
|
15724
17799
|
const seen = new Set();
|
|
15725
17800
|
const rows = [];
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
|
|
15732
|
-
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
17801
|
+
let examinedEdges = 0;
|
|
17802
|
+
const resultFull = () => rows.length >= cap;
|
|
17803
|
+
const refs = db.query('SELECT r.* FROM "references" r WHERE r.calleeFile = ? AND r.callerSymbol IS NOT NULL AND EXISTS (SELECT 1 FROM symbols s WHERE s.filePath = r.calleeFile AND s.name = r.calleeSymbol) ORDER BY r.id LIMIT ?');
|
|
17804
|
+
const referenceSafe = apexReferenceReadGuard(db);
|
|
17805
|
+
try {
|
|
17806
|
+
// Preserve direct named callers; file dependency rows do not invent symbol identities.
|
|
17807
|
+
for (const file of files) {
|
|
17808
|
+
if (examinedEdges >= maxEdges) {
|
|
17809
|
+
reasons.add("work-limit");
|
|
17810
|
+
break;
|
|
17811
|
+
}
|
|
17812
|
+
const candidates = refs.all(file, maxEdges - examinedEdges + 1);
|
|
17813
|
+
for (const ref of candidates) {
|
|
17814
|
+
if (examinedEdges >= maxEdges) {
|
|
17815
|
+
reasons.add("work-limit");
|
|
17816
|
+
break;
|
|
17817
|
+
}
|
|
17818
|
+
examinedEdges++;
|
|
17819
|
+
if (!ref.callerSymbol ||
|
|
17820
|
+
seeds.has(ref.callerFile) ||
|
|
17821
|
+
!referenceSafe(ref) ||
|
|
17822
|
+
!canonicalFile(ref.callerFile) ||
|
|
17823
|
+
!allowed(ref.kind, ref.confidence ?? 1, ref.callerFile))
|
|
17824
|
+
continue;
|
|
17825
|
+
const key = JSON.stringify([ref.callerSymbol, ref.callerFile]);
|
|
17826
|
+
if (seen.has(key))
|
|
17827
|
+
continue;
|
|
17828
|
+
seen.add(key);
|
|
17829
|
+
if (resultFull()) {
|
|
17830
|
+
reasons.add("result-limit");
|
|
17831
|
+
break;
|
|
17832
|
+
}
|
|
17833
|
+
rows.push({
|
|
17834
|
+
symbol: ref.callerSymbol,
|
|
17835
|
+
file: ref.callerFile,
|
|
17836
|
+
line: ref.line,
|
|
17837
|
+
depth: 1,
|
|
17838
|
+
impactEdge: {
|
|
17839
|
+
from: ref.callerSymbol,
|
|
17840
|
+
fromFile: ref.callerFile,
|
|
17841
|
+
to: ref.calleeSymbol,
|
|
17842
|
+
toFile: file,
|
|
17843
|
+
kind: ref.kind,
|
|
17844
|
+
confidence: ref.confidence ?? 1,
|
|
17845
|
+
line: ref.line,
|
|
17846
|
+
},
|
|
17847
|
+
});
|
|
17848
|
+
}
|
|
17849
|
+
if (reasons.has("result-limit"))
|
|
17850
|
+
break;
|
|
15752
17851
|
}
|
|
15753
17852
|
}
|
|
15754
|
-
|
|
15755
|
-
|
|
17853
|
+
finally {
|
|
17854
|
+
refs.finalize();
|
|
17855
|
+
}
|
|
17856
|
+
const incoming = db.query("SELECT id,fromFile,toFile,kind,confidence,sourceEvidence FROM dependencies WHERE toFile = ? ORDER BY id LIMIT ?");
|
|
17857
|
+
const visited = new Set(files);
|
|
17858
|
+
const queue = files.map((file) => ({ file, depth: 0 }));
|
|
17859
|
+
try {
|
|
17860
|
+
for (let index = 0; index < queue.length; index++) {
|
|
17861
|
+
if (reasons.has("result-limit"))
|
|
17862
|
+
break;
|
|
17863
|
+
if (examinedEdges >= maxEdges) {
|
|
17864
|
+
reasons.add("work-limit");
|
|
17865
|
+
break;
|
|
17866
|
+
}
|
|
17867
|
+
const current = queue[index];
|
|
17868
|
+
for (const edge of incoming.all(current.file, maxEdges - examinedEdges + 1)) {
|
|
17869
|
+
if (examinedEdges >= maxEdges) {
|
|
17870
|
+
reasons.add("work-limit");
|
|
17871
|
+
break;
|
|
17872
|
+
}
|
|
17873
|
+
examinedEdges++;
|
|
17874
|
+
if (!isFileRelationship(edge.kind, edge.fromFile, true) ||
|
|
17875
|
+
!isFileRelationship(edge.kind, edge.toFile, false)) {
|
|
17876
|
+
omissions.add("Unsupported or non-file dependency kinds were not traversed.");
|
|
17877
|
+
continue;
|
|
17878
|
+
}
|
|
17879
|
+
if (!canonicalFile(edge.fromFile) || !canonicalFile(edge.toFile)) {
|
|
17880
|
+
omissions.add("Noncanonical, missing, symlinked, or unindexed dependency endpoints were not traversed.");
|
|
17881
|
+
continue;
|
|
17882
|
+
}
|
|
17883
|
+
const confidence = edge.confidence ?? 1;
|
|
17884
|
+
if (!Number.isFinite(confidence) ||
|
|
17885
|
+
confidence < 0 ||
|
|
17886
|
+
confidence > 1 ||
|
|
17887
|
+
!allowed(edge.kind, confidence, edge.fromFile) ||
|
|
17888
|
+
visited.has(edge.fromFile))
|
|
17889
|
+
continue;
|
|
17890
|
+
if (current.depth >= maxDepth) {
|
|
17891
|
+
reasons.add("depth-limit");
|
|
17892
|
+
continue;
|
|
17893
|
+
}
|
|
17894
|
+
if (visited.size >= maxFiles) {
|
|
17895
|
+
reasons.add("file-limit");
|
|
17896
|
+
continue;
|
|
17897
|
+
}
|
|
17898
|
+
if (resultFull()) {
|
|
17899
|
+
reasons.add("result-limit");
|
|
17900
|
+
break;
|
|
17901
|
+
}
|
|
17902
|
+
visited.add(edge.fromFile);
|
|
17903
|
+
queue.push({ file: edge.fromFile, depth: current.depth + 1 });
|
|
17904
|
+
rows.push({
|
|
17905
|
+
file: edge.fromFile,
|
|
17906
|
+
kind: edge.kind,
|
|
17907
|
+
depth: current.depth + 1,
|
|
17908
|
+
...(edge.sourceEvidence ? { sourceEvidence: edge.sourceEvidence } : {}),
|
|
17909
|
+
impactEdge: {
|
|
17910
|
+
from: edge.fromFile,
|
|
17911
|
+
fromFile: edge.fromFile,
|
|
17912
|
+
to: edge.toFile,
|
|
17913
|
+
toFile: edge.toFile,
|
|
17914
|
+
kind: edge.kind,
|
|
17915
|
+
confidence,
|
|
17916
|
+
...(edge.sourceEvidence ? { sourceEvidence: edge.sourceEvidence } : {}),
|
|
17917
|
+
},
|
|
17918
|
+
});
|
|
17919
|
+
}
|
|
17920
|
+
}
|
|
17921
|
+
}
|
|
17922
|
+
finally {
|
|
17923
|
+
incoming.finalize();
|
|
17924
|
+
indexedFile.finalize();
|
|
17925
|
+
}
|
|
17926
|
+
omissions.add("File mode reports direct named callers and bounded upstream file dependencies, not runtime reachability; whole-graph flow/community decoration is omitted.");
|
|
17927
|
+
return finish(rows, {
|
|
17928
|
+
...(reasons.size ? { hasMore: true } : {}),
|
|
17929
|
+
impact: {
|
|
17930
|
+
mode,
|
|
17931
|
+
direction: "upstream",
|
|
17932
|
+
maxDepth,
|
|
17933
|
+
heuristic: true,
|
|
17934
|
+
summary: [
|
|
17935
|
+
rows.length,
|
|
17936
|
+
"heuristic upstream file-level results; bounded dependency traversal",
|
|
17937
|
+
reasons.size ? "(partial)" : "",
|
|
17938
|
+
]
|
|
17939
|
+
.filter(Boolean)
|
|
17940
|
+
.join(" "),
|
|
17941
|
+
affectedFlows: [],
|
|
17942
|
+
affectedCommunities: [],
|
|
17943
|
+
traversal: {
|
|
17944
|
+
maxFiles,
|
|
17945
|
+
maxEdges,
|
|
17946
|
+
maxSeeds,
|
|
17947
|
+
visitedFiles: visited.size,
|
|
17948
|
+
examinedEdges,
|
|
17949
|
+
truncated: reasons.size > 0,
|
|
17950
|
+
reasons: [...reasons],
|
|
17951
|
+
omissions: [...omissions],
|
|
17952
|
+
countIsLowerBound: reasons.size > 0,
|
|
17953
|
+
},
|
|
17954
|
+
},
|
|
17955
|
+
});
|
|
15756
17956
|
}
|
|
15757
17957
|
if (!selectedTarget)
|
|
15758
17958
|
return decorate([], direction);
|
|
@@ -16040,10 +18240,31 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
16040
18240
|
// not recognized (for example, a dynamic loader). Keep it live.
|
|
16041
18241
|
return !matchedTarget;
|
|
16042
18242
|
};
|
|
16043
|
-
const
|
|
18243
|
+
const hasApexDeclarations = db
|
|
18244
|
+
.query("PRAGMA table_info(symbols)")
|
|
18245
|
+
.all()
|
|
18246
|
+
.some((column) => column.name === "apexDeclaration");
|
|
18247
|
+
const stmt = db.query(`SELECT id, identity, name, kind, filePath, startLine, ${hasApexDeclarations ? "apexDeclaration" : "NULL AS apexDeclaration"} FROM symbols WHERE kind IN ('function', 'class', 'method')`);
|
|
16044
18248
|
let rows = stmt.all();
|
|
16045
18249
|
stmt.finalize();
|
|
18250
|
+
if (target)
|
|
18251
|
+
rows = rows.filter((row) => row.filePath.includes(target));
|
|
18252
|
+
const declarations = [];
|
|
18253
|
+
const unsupportedApexFiles = new Set();
|
|
18254
|
+
for (const row of rows) {
|
|
18255
|
+
if (!/\.cls$/i.test(row.filePath))
|
|
18256
|
+
continue;
|
|
18257
|
+
const declaration = parseApexDeclaration(row.apexDeclaration);
|
|
18258
|
+
if (declaration)
|
|
18259
|
+
declarations.push({ key: String(row.id), file: row.filePath, declaration });
|
|
18260
|
+
else
|
|
18261
|
+
unsupportedApexFiles.add(row.filePath);
|
|
18262
|
+
}
|
|
18263
|
+
const protectedApexEntries = apexOrgEntryPoints(declarations);
|
|
18264
|
+
const capability = apexReceiverCapability(db);
|
|
16046
18265
|
rows = rows.filter((r) => {
|
|
18266
|
+
if (protectedApexEntries.has(String(r.id)) || unsupportedApexFiles.has(r.filePath))
|
|
18267
|
+
return false;
|
|
16047
18268
|
if (r.kind === "variable")
|
|
16048
18269
|
return false;
|
|
16049
18270
|
if (r.name.length <= 3)
|
|
@@ -16082,6 +18303,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
16082
18303
|
file: r.filePath,
|
|
16083
18304
|
line: r.startLine,
|
|
16084
18305
|
heuristic: true,
|
|
18306
|
+
identity: r.identity ?? undefined,
|
|
16085
18307
|
sourceEvidence: "No statically resolved references; runtime dispatch, reflection, aliases, and indirect exports remain unresolved.",
|
|
16086
18308
|
drillDown: {
|
|
16087
18309
|
operation: "query",
|
|
@@ -16090,7 +18312,21 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
16090
18312
|
file: r.filePath,
|
|
16091
18313
|
limit: 25,
|
|
16092
18314
|
},
|
|
16093
|
-
}))
|
|
18315
|
+
})), {
|
|
18316
|
+
deadCodeAnalysis: {
|
|
18317
|
+
protectedApexEntries: protectedApexEntries.size,
|
|
18318
|
+
unsupportedApexFiles: unsupportedApexFiles.size,
|
|
18319
|
+
repositoryMissingApexCapabilityFiles: capability.missingFiles,
|
|
18320
|
+
partial: unsupportedApexFiles.size > 0 || capability.partial,
|
|
18321
|
+
limitations: [
|
|
18322
|
+
"Org-facing Apex declarations are excluded; this does not prove runtime use or root reachability. Global visibility alone is not an exemption.",
|
|
18323
|
+
"Remaining results are static candidates, not safe-to-delete findings; dead chains and dynamic dispatch are not established.",
|
|
18324
|
+
...(unsupportedApexFiles.size > 0 || capability.partial
|
|
18325
|
+
? [capability.repair]
|
|
18326
|
+
: []),
|
|
18327
|
+
],
|
|
18328
|
+
},
|
|
18329
|
+
});
|
|
16094
18330
|
}
|
|
16095
18331
|
case "cross_substrate_path": {
|
|
16096
18332
|
const unsupportedCrossings = [
|
|
@@ -16147,7 +18383,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
16147
18383
|
...flowSource.matchAll(/<actionCalls>([\s\S]*?)<\/actionCalls>/g),
|
|
16148
18384
|
]
|
|
16149
18385
|
.filter((match) => /<actionType>apex<\/actionType>/.test(match[1]))
|
|
16150
|
-
.filter((match) => staticXmlTagValues(match[1], "actionName").some((entry) => entry.value === actionName
|
|
18386
|
+
.filter((match) => staticXmlTagValues(match[1], "actionName").some((entry) => entry.value === actionName ||
|
|
18387
|
+
(entry.value === className &&
|
|
18388
|
+
findUniqueInvocableApexMethod(className, undefined, primary.path)?.method ===
|
|
18389
|
+
destination.selected?.name)));
|
|
16151
18390
|
if (matchingActions.length === 1)
|
|
16152
18391
|
flowEvidence = matchingActions[0][0].trim();
|
|
16153
18392
|
}
|
|
@@ -17642,6 +19881,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
17642
19881
|
}
|
|
17643
19882
|
for (const repo of releasable) {
|
|
17644
19883
|
const db = dbInstances.get(repo);
|
|
19884
|
+
if (db)
|
|
19885
|
+
apexConsumerValidationMemo.delete(db);
|
|
17645
19886
|
if (db)
|
|
17646
19887
|
db.close();
|
|
17647
19888
|
dbInstances.delete(repo);
|
|
@@ -17650,6 +19891,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
17650
19891
|
}
|
|
17651
19892
|
for (const key of explicitDatabaseKeys) {
|
|
17652
19893
|
const db = dbInstances.get(key);
|
|
19894
|
+
if (db)
|
|
19895
|
+
apexConsumerValidationMemo.delete(db);
|
|
17653
19896
|
if (db)
|
|
17654
19897
|
db.close();
|
|
17655
19898
|
dbInstances.delete(key);
|