sf-intelligence 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1335 -110
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1978,7 +1978,7 @@ var init_resolve_index = __esm({
|
|
|
1978
1978
|
});
|
|
1979
1979
|
|
|
1980
1980
|
// ../graph/dist/src/resolve.js
|
|
1981
|
-
var DEFAULT_LIMIT, MAX_LIMIT, MIN_BASE, MATCHED_FLOOR, NONE_THRESHOLD, EXACT_THRESHOLD, EXACT_COVERAGE, PREFIX_EXACT_RATIO, CONTENDER_RATIO, LENGTH_RATIO_FLOOR, SYNONYM_SCORE, POP_K, COVERAGE_EXP, STRONG_ANCHOR, INTERNAL_RESOLVE_TYPES, queryNamesComponentType, TYPE_WEIGHT, typeWeight, isPureShortSubstringOfCompound, scoreToken, rollupKind, buildEvidence, resolveComponents;
|
|
1981
|
+
var DEFAULT_LIMIT, MAX_LIMIT, MIN_BASE, MATCHED_FLOOR, NONE_THRESHOLD, EXACT_THRESHOLD, EXACT_COVERAGE, PREFIX_EXACT_RATIO, CONTENDER_RATIO, LENGTH_RATIO_FLOOR, SYNONYM_SCORE, POP_K, COVERAGE_EXP, STRONG_ANCHOR, INTERNAL_RESOLVE_TYPES, queryNamesComponentType, TYPE_HINT_SINGLE_NOUNS, TYPE_HINT_NOUN_PAIRS, stripTypeHintNouns, TYPE_WEIGHT, typeWeight, isPureShortSubstringOfCompound, scoreToken, rollupKind, buildEvidence, resolveComponents;
|
|
1982
1982
|
var init_resolve = __esm({
|
|
1983
1983
|
"../graph/dist/src/resolve.js"() {
|
|
1984
1984
|
"use strict";
|
|
@@ -2012,8 +2012,69 @@ var init_resolve = __esm({
|
|
|
2012
2012
|
return "ApexClass";
|
|
2013
2013
|
if (/\btriggers?\b/.test(lower))
|
|
2014
2014
|
return "ApexTrigger";
|
|
2015
|
+
if (/\bpermission\s+sets?\b/.test(lower))
|
|
2016
|
+
return "PermissionSet";
|
|
2017
|
+
if (/\brecord\s+types?\b/.test(lower))
|
|
2018
|
+
return "RecordType";
|
|
2019
|
+
if (/\bprofiles?\b/.test(lower))
|
|
2020
|
+
return "Profile";
|
|
2015
2021
|
return null;
|
|
2016
2022
|
};
|
|
2023
|
+
TYPE_HINT_SINGLE_NOUNS = /* @__PURE__ */ new Set([
|
|
2024
|
+
"trigger",
|
|
2025
|
+
"triggers",
|
|
2026
|
+
"profile",
|
|
2027
|
+
"profiles",
|
|
2028
|
+
"field",
|
|
2029
|
+
"fields",
|
|
2030
|
+
"object",
|
|
2031
|
+
"objects",
|
|
2032
|
+
"flow",
|
|
2033
|
+
"flows",
|
|
2034
|
+
"component",
|
|
2035
|
+
"components"
|
|
2036
|
+
]);
|
|
2037
|
+
TYPE_HINT_NOUN_PAIRS = [
|
|
2038
|
+
["permission", "set"],
|
|
2039
|
+
["permission", "sets"],
|
|
2040
|
+
["record", "type"],
|
|
2041
|
+
["record", "types"]
|
|
2042
|
+
];
|
|
2043
|
+
stripTypeHintNouns = (raw) => {
|
|
2044
|
+
const words = raw.trim().split(/\s+/);
|
|
2045
|
+
const norm = (w2) => w2.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2046
|
+
let start = 0;
|
|
2047
|
+
let end = words.length;
|
|
2048
|
+
const isPairAt = (i2) => i2 >= 0 && i2 + 1 < end && TYPE_HINT_NOUN_PAIRS.some(([a2, b2]) => norm(words[i2]) === a2 && norm(words[i2 + 1]) === b2);
|
|
2049
|
+
for (; ; ) {
|
|
2050
|
+
if (start < end && /^(?:the|a|an)$/i.test(norm(words[start]))) {
|
|
2051
|
+
start += 1;
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (isPairAt(start) && start + 2 < end) {
|
|
2055
|
+
start += 2;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
if (start < end - 1 && TYPE_HINT_SINGLE_NOUNS.has(norm(words[start]))) {
|
|
2059
|
+
start += 1;
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
break;
|
|
2063
|
+
}
|
|
2064
|
+
for (; ; ) {
|
|
2065
|
+
if (isPairAt(end - 2) && end - 2 > start) {
|
|
2066
|
+
end -= 2;
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (end - 1 > start && TYPE_HINT_SINGLE_NOUNS.has(norm(words[end - 1]))) {
|
|
2070
|
+
end -= 1;
|
|
2071
|
+
continue;
|
|
2072
|
+
}
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
const kept = words.slice(start, end).join(" ");
|
|
2076
|
+
return kept.length > 0 ? kept : raw;
|
|
2077
|
+
};
|
|
2017
2078
|
TYPE_WEIGHT = {
|
|
2018
2079
|
CustomObject: 1,
|
|
2019
2080
|
CustomField: 0.95,
|
|
@@ -2083,8 +2144,21 @@ var init_resolve = __esm({
|
|
|
2083
2144
|
resolveComponents = async (store, query, options) => {
|
|
2084
2145
|
const limit = Math.min(options?.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
2085
2146
|
const minBase = options?.minScore ?? MIN_BASE;
|
|
2086
|
-
const
|
|
2147
|
+
const strippedQuery = stripTypeHintNouns(query);
|
|
2148
|
+
const queryTokens = tokenizeText(strippedQuery, { expandPhrases: true });
|
|
2087
2149
|
const normQuery = normalizeName(query);
|
|
2150
|
+
const normStrippedQuery = strippedQuery.length < query.trim().length ? normalizeName(strippedQuery) : normQuery;
|
|
2151
|
+
const queryTrimmed = query.trim();
|
|
2152
|
+
const normAfterTypePrefix = (() => {
|
|
2153
|
+
if (!queryTrimmed.includes(":") || /\s/.test(queryTrimmed))
|
|
2154
|
+
return normQuery;
|
|
2155
|
+
const colonIdx = queryTrimmed.indexOf(":");
|
|
2156
|
+
const afterColon = queryTrimmed.slice(colonIdx + 1);
|
|
2157
|
+
if (afterColon.includes(":") || afterColon.includes("."))
|
|
2158
|
+
return normQuery;
|
|
2159
|
+
const n2 = normalizeName(afterColon);
|
|
2160
|
+
return n2.length >= 2 ? n2 : normQuery;
|
|
2161
|
+
})();
|
|
2088
2162
|
const querySpaceWords = new Set(query.trim().split(/\s+/).map(normalizeName).filter((w2) => w2.length > 0));
|
|
2089
2163
|
let dottedObjectNorm = null;
|
|
2090
2164
|
let dottedFieldNorm = null;
|
|
@@ -2110,7 +2184,20 @@ var init_resolve = __esm({
|
|
|
2110
2184
|
message: `resolveComponents: ${e2.message}`
|
|
2111
2185
|
});
|
|
2112
2186
|
}
|
|
2113
|
-
const
|
|
2187
|
+
const candidateIdxSet = new Set(gatherCandidates(index, queryTokens, normQuery));
|
|
2188
|
+
if (normStrippedQuery !== normQuery) {
|
|
2189
|
+
const extra = index.byNormName.get(normStrippedQuery);
|
|
2190
|
+
if (extra !== void 0)
|
|
2191
|
+
for (const i2 of extra)
|
|
2192
|
+
candidateIdxSet.add(i2);
|
|
2193
|
+
}
|
|
2194
|
+
if (normAfterTypePrefix !== normQuery && normAfterTypePrefix !== normStrippedQuery) {
|
|
2195
|
+
const extra = index.byNormName.get(normAfterTypePrefix);
|
|
2196
|
+
if (extra !== void 0)
|
|
2197
|
+
for (const i2 of extra)
|
|
2198
|
+
candidateIdxSet.add(i2);
|
|
2199
|
+
}
|
|
2200
|
+
const candidateIdx = [...candidateIdxSet];
|
|
2114
2201
|
const objectNormNames = /* @__PURE__ */ new Set();
|
|
2115
2202
|
for (const n2 of index.nodes) {
|
|
2116
2203
|
if (n2.type === "CustomObject")
|
|
@@ -2161,7 +2248,8 @@ var init_resolve = __esm({
|
|
|
2161
2248
|
}
|
|
2162
2249
|
const crossObjectFieldDecoy = node.type === "CustomField" && namedObjectWords.size > 0 && (node.parentApiName === null || !namedObjectWords.has(normalizeName(node.parentApiName)));
|
|
2163
2250
|
const dottedExact = dottedObjectNorm !== null && dottedFieldNorm !== null && node.parentApiName !== null && normalizeName(node.parentApiName) === dottedObjectNorm && normalizeName(node.apiName) === dottedFieldNorm;
|
|
2164
|
-
const
|
|
2251
|
+
const wholeExactNormMatch = normQuery.length >= 2 && node.normName === normQuery || normStrippedQuery !== normQuery && normStrippedQuery.length >= 2 && node.normName === normStrippedQuery || normAfterTypePrefix !== normQuery && normAfterTypePrefix.length >= 2 && node.normName === normAfterTypePrefix;
|
|
2252
|
+
const wholeExact = dottedExact || wholeExactNormMatch && query.includes(".") === node.apiName.includes(".") && !crossObjectFieldDecoy;
|
|
2165
2253
|
pass1.push({ node, perToken, wholeExact, parentMatched });
|
|
2166
2254
|
}
|
|
2167
2255
|
const anchorIdx = [];
|
|
@@ -2255,12 +2343,6 @@ var init_resolve = __esm({
|
|
|
2255
2343
|
return 1;
|
|
2256
2344
|
};
|
|
2257
2345
|
scored.sort((a2, b2) => {
|
|
2258
|
-
if (namedTypeForSort !== null) {
|
|
2259
|
-
const ttA = typeIntentTier(a2.type);
|
|
2260
|
-
const ttB = typeIntentTier(b2.type);
|
|
2261
|
-
if (ttA !== ttB)
|
|
2262
|
-
return ttA - ttB;
|
|
2263
|
-
}
|
|
2264
2346
|
const weA = wholeExactIds.has(a2.id) ? 0 : 1;
|
|
2265
2347
|
const weB = wholeExactIds.has(b2.id) ? 0 : 1;
|
|
2266
2348
|
if (weA !== weB)
|
|
@@ -2269,6 +2351,12 @@ var init_resolve = __esm({
|
|
|
2269
2351
|
const tierB = b2.base >= EXACT_THRESHOLD ? 0 : 1;
|
|
2270
2352
|
if (tierA !== tierB)
|
|
2271
2353
|
return tierA - tierB;
|
|
2354
|
+
if (namedTypeForSort !== null) {
|
|
2355
|
+
const ttA = typeIntentTier(a2.type);
|
|
2356
|
+
const ttB = typeIntentTier(b2.type);
|
|
2357
|
+
if (ttA !== ttB)
|
|
2358
|
+
return ttA - ttB;
|
|
2359
|
+
}
|
|
2272
2360
|
const pmA = parentMatchedIds.has(a2.id) ? 0 : 1;
|
|
2273
2361
|
const pmB = parentMatchedIds.has(b2.id) ? 0 : 1;
|
|
2274
2362
|
if (pmA !== pmB)
|
|
@@ -24094,7 +24182,7 @@ ${renderTrustFooter(trust)}`;
|
|
|
24094
24182
|
import { appendFile as appendFile3, mkdir as mkdir8 } from "node:fs/promises";
|
|
24095
24183
|
import { homedir as homedir3 } from "node:os";
|
|
24096
24184
|
import { dirname as dirname18, join as join13 } from "node:path";
|
|
24097
|
-
var riskForIntent, routeFromRule, routeForSelectedIntent, normalize, deriveSaveEvent, deriveKnowledgeTopic, deriveListType, FIELD_PARENT_OBJECTS, deriveFieldListParent, deriveMetadataParentId, deriveObjectApiFromQuestion, deriveImpactHops, FIELD_MAP_OBJECT, deriveFieldMappingArgs, deriveLayoutForUserArgs, derivePiiInventoryArgs, deriveMetadataCountArgs, deriveOmniType, routeText, RULES, alternativeFromIntent, semanticAlternatives, intentLabel, classifyQuestion, gapLogPath, logGapIfAny;
|
|
24185
|
+
var riskForIntent, routeFromRule, routeForSelectedIntent, normalize, NAMED_COMPONENT_ID, NAMED_FIELD_ID, deriveSaveEvent, deriveKnowledgeTopic, deriveListType, FIELD_PARENT_OBJECTS, deriveFieldListParent, deriveMetadataParentId, NON_OBJECT_CAPTURES, deriveObjectApiFromQuestion, deriveImpactHops, FIELD_MAP_OBJECT, deriveFieldMappingArgs, deriveLayoutForUserArgs, derivePiiInventoryArgs, deriveMetadataCountArgs, deriveOmniType, routeText, RULES, alternativeFromIntent, semanticAlternatives, intentLabel, classifyQuestion, gapLogPath, logGapIfAny;
|
|
24098
24186
|
var init_intent_router = __esm({
|
|
24099
24187
|
"../mcp/dist/src/intent-router.js"() {
|
|
24100
24188
|
"use strict";
|
|
@@ -24149,6 +24237,8 @@ var init_intent_router = __esm({
|
|
|
24149
24237
|
};
|
|
24150
24238
|
};
|
|
24151
24239
|
normalize = (q2) => q2.trim().toLowerCase().replace(/[‘’ʼ]/g, "'").replace(/\s+/g, " ");
|
|
24240
|
+
NAMED_COMPONENT_ID = "(?!\\w*__)[a-z][a-z0-9]*_[a-z0-9]+_[a-z0-9_]*[a-z0-9]";
|
|
24241
|
+
NAMED_FIELD_ID = "(?:[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*|[a-z][a-z0-9_]*__c)";
|
|
24152
24242
|
deriveSaveEvent = (q2) => {
|
|
24153
24243
|
if (/\b(undelet|restor)/.test(q2))
|
|
24154
24244
|
return "undelete";
|
|
@@ -24183,6 +24273,8 @@ var init_intent_router = __esm({
|
|
|
24183
24273
|
return "apex-testing";
|
|
24184
24274
|
if (/\bstandard\s+profiles?\b.*\b(ship|new\s+org)\b/.test(q2))
|
|
24185
24275
|
return "profiles-vs-permission-sets";
|
|
24276
|
+
if (/\bwhat\s+is\s+an?\s+(?:profile|permission\s+set)\b/.test(q2))
|
|
24277
|
+
return "profiles-vs-permission-sets";
|
|
24186
24278
|
if (/\b(person\s+accounts?)\b/.test(q2))
|
|
24187
24279
|
return "standard-vs-custom-objects";
|
|
24188
24280
|
if (/\b(ci\/cd|source\s+control|deployment\s+(and\s+)?release)\b/.test(q2))
|
|
@@ -24276,14 +24368,41 @@ var init_intent_router = __esm({
|
|
|
24276
24368
|
return `CustomObject:${onObject[1]}`;
|
|
24277
24369
|
return void 0;
|
|
24278
24370
|
};
|
|
24371
|
+
NON_OBJECT_CAPTURES = /* @__PURE__ */ new Set([
|
|
24372
|
+
"save",
|
|
24373
|
+
"saves",
|
|
24374
|
+
"saving",
|
|
24375
|
+
"insert",
|
|
24376
|
+
"update",
|
|
24377
|
+
"delete",
|
|
24378
|
+
"undelete",
|
|
24379
|
+
"create",
|
|
24380
|
+
"creation",
|
|
24381
|
+
"edit",
|
|
24382
|
+
"record",
|
|
24383
|
+
"records",
|
|
24384
|
+
"object",
|
|
24385
|
+
"objects",
|
|
24386
|
+
"it",
|
|
24387
|
+
"this",
|
|
24388
|
+
"that",
|
|
24389
|
+
"them",
|
|
24390
|
+
"each",
|
|
24391
|
+
"every",
|
|
24392
|
+
"all"
|
|
24393
|
+
]);
|
|
24279
24394
|
deriveObjectApiFromQuestion = (q2, question) => {
|
|
24280
24395
|
const source = question ?? q2;
|
|
24281
24396
|
const toolObject = source.match(/\b(?:automation_build_advisor|order_of_execution|apex_build_advisor)\b\s+(?:on\s+)?([A-Za-z][A-Za-z0-9_]*(?:__c|__mdt|__e)?)\b/i);
|
|
24282
24397
|
if (toolObject?.[1] !== void 0)
|
|
24283
24398
|
return toolObject[1];
|
|
24284
|
-
const
|
|
24285
|
-
|
|
24286
|
-
|
|
24399
|
+
const onObjectRe = /\b(?:on|for|to|access\s+to)\s+(?:the\s+|an\s+|a\s+)?([A-Za-z][A-Za-z0-9_]*(?:__c|__mdt|__e)?)\b/gi;
|
|
24400
|
+
for (const match of source.matchAll(onObjectRe)) {
|
|
24401
|
+
const capture = match[1];
|
|
24402
|
+
if (capture !== void 0 && !NON_OBJECT_CAPTURES.has(capture.toLowerCase())) {
|
|
24403
|
+
return capture;
|
|
24404
|
+
}
|
|
24405
|
+
}
|
|
24287
24406
|
const dmlObject = source.match(/\b(?:update|insert|delete|save|create|edit)\s+(?:a\s+|an\s+|the\s+)?([A-Za-z][A-Za-z0-9_]*(?:__c|__mdt|__e)?)\b/);
|
|
24288
24407
|
if (dmlObject?.[1] !== void 0)
|
|
24289
24408
|
return dmlObject[1];
|
|
@@ -24621,7 +24740,13 @@ var init_intent_router = __esm({
|
|
|
24621
24740
|
liveRequired: false,
|
|
24622
24741
|
needsResolve: true,
|
|
24623
24742
|
reason: "Explicit downstream_effects invocation.",
|
|
24624
|
-
patterns: [
|
|
24743
|
+
patterns: [
|
|
24744
|
+
/\bdownstream_effects\b/,
|
|
24745
|
+
// "downstream effects of changing X" — the natural phrasing. Previously
|
|
24746
|
+
// caught only by pii-flow's bare \bdownstream\b catch-all (eval family E
|
|
24747
|
+
// demoted that), so the dedicated tool now owns its own noun phrase.
|
|
24748
|
+
/\b(?:downstream|ripple)\s+effects?\b/
|
|
24749
|
+
]
|
|
24625
24750
|
},
|
|
24626
24751
|
{
|
|
24627
24752
|
intent: "package-impact",
|
|
@@ -24679,7 +24804,17 @@ var init_intent_router = __esm({
|
|
|
24679
24804
|
liveRequired: false,
|
|
24680
24805
|
needsResolve: true,
|
|
24681
24806
|
reason: "Explicit async_chain_depth invocation.",
|
|
24682
|
-
patterns: [
|
|
24807
|
+
patterns: [
|
|
24808
|
+
/\basync_chain_depth\b/,
|
|
24809
|
+
// NL async-chain shapes (flow-family REACH). High precision: an explicit
|
|
24810
|
+
// async-chain-depth / async-Apex-limit concern (queueable→queueable /
|
|
24811
|
+
// future/batch chaining against the 5-deep / async-Apex governor limit).
|
|
24812
|
+
/\basync\s+chain\b/,
|
|
24813
|
+
/\basync[-\s]?apex[-\s]?limit\b/,
|
|
24814
|
+
/\basync[-\s]apex[-\s]limit\b/,
|
|
24815
|
+
/\bhow\s+deep\b[^.?!]{0,40}\basync\b/,
|
|
24816
|
+
/\b(queueable|future|batch)\b[^.?!]{0,40}\bchain\w*\b[^.?!]{0,40}\b(depth|deep|limit)\b/
|
|
24817
|
+
]
|
|
24683
24818
|
},
|
|
24684
24819
|
{
|
|
24685
24820
|
intent: "compliance",
|
|
@@ -24940,6 +25075,32 @@ var init_intent_router = __esm({
|
|
|
24940
25075
|
/\bhow\s+many\s+users?\b.*\bon\b.*\bprofiles?\b/
|
|
24941
25076
|
]
|
|
24942
25077
|
},
|
|
25078
|
+
{
|
|
25079
|
+
// "list everyone with the X profile" is a USER ROSTER ask: user-to-profile
|
|
25080
|
+
// assignment is runtime User-record state, not vault metadata — the schema
|
|
25081
|
+
// list rule used to claim it via "list ... profiles" and answer with the
|
|
25082
|
+
// Profile METADATA catalog (eval family D). live_group_count genuinely
|
|
25083
|
+
// answers the count side (Users grouped by ProfileId) and
|
|
25084
|
+
// live_inactive_users the login-activity side; the name-by-name roster
|
|
25085
|
+
// itself is disclosed as a partial answer, never papered over.
|
|
25086
|
+
intent: "profile-user-roster",
|
|
25087
|
+
plane: "live",
|
|
25088
|
+
tools: ["sfi.live_group_count", "sfi.live_inactive_users"],
|
|
25089
|
+
liveRequired: true,
|
|
25090
|
+
needsResolve: false,
|
|
25091
|
+
reason: "Which users hold a profile is runtime User-record state: count them live grouped by ProfileId (live_group_count); login-activity detail comes from live_inactive_users.",
|
|
25092
|
+
gap: {
|
|
25093
|
+
category: "profile-user-roster",
|
|
25094
|
+
note: "Partial answer: live_group_count returns user COUNTS per profile and live_inactive_users lists dormant users, but a full name-by-name user roster per profile is not a built capability yet."
|
|
25095
|
+
},
|
|
25096
|
+
suggestArgs: () => ({ objectApiName: "User", groupByField: "ProfileId" }),
|
|
25097
|
+
patterns: [
|
|
25098
|
+
/\b(list|show|who\s+are)\b[^.?!]{0,20}\b(everyone|everybody|all\s+(the\s+)?users?|the\s+users?|people)\b[^.?!]{0,40}\b(with|on|assigned|holding|having)\b[^.?!]{0,40}\bprofile\b/,
|
|
25099
|
+
/\b(which|what)\s+users?\b[^.?!]{0,40}\b(have|hold|are\s+on|with|assigned)\b[^.?!]{0,40}\bprofile\b/,
|
|
25100
|
+
/\bwho\s+(has|holds|is\s+assigned)\b[^.?!]{0,40}\bprofile\b/,
|
|
25101
|
+
/\beveryone\b[^.?!]{0,30}\b(with|on|assigned)\b[^.?!]{0,40}\bprofile\b/
|
|
25102
|
+
]
|
|
25103
|
+
},
|
|
24943
25104
|
{
|
|
24944
25105
|
intent: "stale-metadata",
|
|
24945
25106
|
plane: "vault",
|
|
@@ -24969,10 +25130,25 @@ var init_intent_router = __esm({
|
|
|
24969
25130
|
needsResolve: false,
|
|
24970
25131
|
reason: "License provisioning/usage and reclaimable seats are live org state (UserLicense / PermissionSetLicense + login activity).",
|
|
24971
25132
|
patterns: [
|
|
24972
|
-
|
|
24973
|
-
|
|
25133
|
+
// Guarded (eval family C — qualifier hijack): "seats"/"license" as a
|
|
25134
|
+
// TRAILING modifier must not drag a permission-set ASSIGNMENT question
|
|
25135
|
+
// ("who is assigned the X permission set — are we wasting seats?") onto
|
|
25136
|
+
// the live license counter. When the question mentions a permission set
|
|
25137
|
+
// that is NOT the literal "permission set license" (PSL) noun, the
|
|
25138
|
+
// perm-set rules below own it; genuine PSL asks keep the third pattern.
|
|
25139
|
+
/^(?!.*\bpermission\s+sets?\b(?!\s+licen[sc]e)).*\b(licen[sc]e|seat)s?\b.*\b(usage|used|unused|utili[sz]ation|utilized|reclaim|reclaimable|available|free|provision|assigned|wasted|cost|optimi[sz])/,
|
|
25140
|
+
/^(?!.*\bpermission\s+sets?\b(?!\s+licen[sc]e)).*\b(usage|utili[sz]ation|utilized|reclaim|reclaimable|unused|provision|assigned|wasted)\b.*\b(licen[sc]e|seat)s?\b/,
|
|
24974
25141
|
/\bpermission\s+set\s+licen[sc]e/,
|
|
24975
|
-
/\bhow\s+many\s+(licen[sc]e|seat)s?\b
|
|
25142
|
+
/\bhow\s+many\s+(licen[sc]e|seat)s?\b/,
|
|
25143
|
+
// DISCOVERY/META REACH: "how many <LicenseType> and <LicenseType>
|
|
25144
|
+
// licenses are we actually USING vs what we're PAYING for" — the
|
|
25145
|
+
// provisioning-vs-consumption ask. The base patterns above keyed on
|
|
25146
|
+
// `used`/`usage` (not the gerund "using") and on "how many licenses"
|
|
25147
|
+
// adjacent, so "how many Salesforce and Community licenses … using"
|
|
25148
|
+
// fell through. Anchor on "how many … licenses … using/paying" (the
|
|
25149
|
+
// provisioned-vs-paid frame), still guarded off a perm-set-assignment
|
|
25150
|
+
// question by the leading negative lookahead style used above.
|
|
25151
|
+
/^(?!.*\bpermission\s+sets?\b(?!\s+licen[sc]e)).*\bhow\s+many\b[^.?!]{0,60}\blicen[sc]es?\b[^.?!]{0,60}\b(?:using|use|paying|pay|provision\w*|actually\s+us\w*)\b/
|
|
24976
25152
|
]
|
|
24977
25153
|
},
|
|
24978
25154
|
{
|
|
@@ -24991,7 +25167,9 @@ var init_intent_router = __esm({
|
|
|
24991
25167
|
// count (record-count) or a permission audit (over-permission), not dormancy.
|
|
24992
25168
|
/\bhow\s+many\b.*\b(inactive|dormant|stale)\b.*\busers?\b/,
|
|
24993
25169
|
/\bhow\s+many\b.*\busers?\b.*\b(inactive|dormant|stale|haven'?t\s+logged|not\s+logged)\b/,
|
|
24994
|
-
|
|
25170
|
+
// Same permission-set guard as license-usage (eval family C): a seat
|
|
25171
|
+
// modifier on a perm-set-assignment ask must not land on login activity.
|
|
25172
|
+
/^(?!.*\bpermission\s+sets?\b(?!\s+licen[sc]e)).*\b(license|seat)s?\b.*\b(reclaim|unused|free|available)\b/
|
|
24995
25173
|
]
|
|
24996
25174
|
},
|
|
24997
25175
|
{
|
|
@@ -25175,7 +25353,14 @@ var init_intent_router = __esm({
|
|
|
25175
25353
|
/\brecord\s+counts?\b.*\b(per|by|across)\b.*\bobjects?\b/,
|
|
25176
25354
|
/\bstorage\b.*\b(by|per)\b.*\bobject\b/,
|
|
25177
25355
|
/\bdata\s+volume\b.*\bobject\b/,
|
|
25178
|
-
/\btop\b.*\bobjects?\b.*\b(by|with)\b.*\b(records?|rows?)\b
|
|
25356
|
+
/\btop\b.*\bobjects?\b.*\b(by|with)\b.*\b(records?|rows?)\b/,
|
|
25357
|
+
// "which custom objects are essentially empty in prod" — whether an
|
|
25358
|
+
// object holds records is the SAME live per-object COUNT read from the
|
|
25359
|
+
// other end (zero/near-zero instead of most); it was unrouted and the
|
|
25360
|
+
// vault cannot answer it at all (eval family D).
|
|
25361
|
+
/\b(which|what)\b[^.?!]{0,40}\bobjects?\b[^.?!]{0,50}\bempty\b/,
|
|
25362
|
+
/\bempty\b[^.?!]{0,20}\b(custom\s+)?objects?\b/,
|
|
25363
|
+
/\bobjects?\b[^.?!]{0,40}\b(no|zero|barely\s+any|hardly\s+any)\s+(records?|rows?|data)\b/
|
|
25179
25364
|
]
|
|
25180
25365
|
},
|
|
25181
25366
|
{
|
|
@@ -25332,7 +25517,11 @@ var init_intent_router = __esm({
|
|
|
25332
25517
|
reason: "Report inventory is in the vault; stale/unused needs live LastRunDate.",
|
|
25333
25518
|
patterns: [
|
|
25334
25519
|
/\breports?\b.*\b(useless|unused|stale|dead|old|never\s+run|not\s+used|broken)\b/,
|
|
25335
|
-
|
|
25520
|
+
// Guarded (eval family C): "compliance report … who touches the X field"
|
|
25521
|
+
// uses "report" as the requested DELIVERABLE, not the subject — the
|
|
25522
|
+
// head question is field access, so a compliance/who-touches frame must
|
|
25523
|
+
// not land on live report run-history.
|
|
25524
|
+
/^(?!.*\b(?:compliance|who\s+touch)\w*\b).*\b(reports?|dashboards?)\b.*\b(cover|covers|about|for)\b(?!.*\breport\s+types?\b)/,
|
|
25336
25525
|
/\b(useless|unused|stale|dead)\b.*\breports?\b/,
|
|
25337
25526
|
/\b(dashboards?)\b.*\b(unused|stale|broken|refresh)\b/,
|
|
25338
25527
|
/\breports?\b.*\b(not\s+run|haven'?t\s+been\s+run)\b/,
|
|
@@ -25402,7 +25591,14 @@ var init_intent_router = __esm({
|
|
|
25402
25591
|
/\b(which|what)\s+profiles?\b.*\bcan\b.*\brun\b.*\breports?\b/,
|
|
25403
25592
|
// API-enabled login profiles.
|
|
25404
25593
|
/\b(which|what)\s+profiles?\b.*\b(api|log\s+in|login)\b/,
|
|
25405
|
-
/\bprofiles?\b.*\b(log\s+in|login)\b.*\bapi\b
|
|
25594
|
+
/\bprofiles?\b.*\b(log\s+in|login)\b.*\bapi\b/,
|
|
25595
|
+
// REACH (permissions/access cluster) — FORWARD run-access: "which screen
|
|
25596
|
+
// flows are exposed / available / visible / assigned to the <Named>
|
|
25597
|
+
// profile / perm set". This is the granter's OWN runnableFlows list
|
|
25598
|
+
// (user_ability), not the reverse who_can_run (which starts from a flow).
|
|
25599
|
+
// "exposed to <profile>" is the natural phrasing the existing
|
|
25600
|
+
// "what flows can X run" templates missed.
|
|
25601
|
+
/\bwhich\s+(?:screen\s+)?flows?\b[^.?!]{0,40}\b(?:exposed|available|visible|assigned)\s+to\b[^.?!]{0,30}\b(?:profile|perm\s*sets?|permission\s+sets?|user)\b/
|
|
25406
25602
|
]
|
|
25407
25603
|
},
|
|
25408
25604
|
{
|
|
@@ -25415,7 +25611,20 @@ var init_intent_router = __esm({
|
|
|
25415
25611
|
patterns: [
|
|
25416
25612
|
/\bwho\s+can\s+run\b.*\bflow/,
|
|
25417
25613
|
/\b(which|what)\s+(profiles?|permission\s+sets?|permsets?)\b.*\brun\b.*\bflow/,
|
|
25418
|
-
/\bwho\s+(can|is\s+able\s+to)\s+run\b.*\bflow
|
|
25614
|
+
/\bwho\s+(can|is\s+able\s+to)\s+run\b.*\bflow/,
|
|
25615
|
+
// REACH (permissions/access cluster): the existing templates broke on
|
|
25616
|
+
// (1) an adverb between who/can and run ("who EXACTLY can run …",
|
|
25617
|
+
// "who ACTUALLY can run …") and (2) a Flow named by its API id with a
|
|
25618
|
+
// `_flow` / `_screen_flow` suffix but no separate bare word "flow"
|
|
25619
|
+
// ("run Some_Named_Screen_Flow"). `FLOWREF` matches the bare word
|
|
25620
|
+
// "flow(s)", the `_flow` suffix (no leading `\b`, so it fires inside a
|
|
25621
|
+
// multi-underscore API name), or "screen flow(s)". Two shapes:
|
|
25622
|
+
// (a) who [adverb] can run … <flow>
|
|
25623
|
+
// (b) can the <profile/perm set/user> run … <flow>
|
|
25624
|
+
// Both require the RUN verb + a flow reference, so they never steal a
|
|
25625
|
+
// record/object/field access ask (no "run … flow" there).
|
|
25626
|
+
/\bwho\b[^.?!]{0,20}\bcan\b[^.?!]{0,15}\brun\b[^.?!]{0,60}(?:\bflows?\b|_flow\b|screen\s+flows?\b)/,
|
|
25627
|
+
/\bcan\b[^.?!]{0,40}\b(?:profile|perm\s*sets?|permission\s+sets?|user)\b[^.?!]{0,20}\brun\b[^.?!]{0,60}(?:\bflows?\b|_flow\b|screen\s+flows?\b)/
|
|
25419
25628
|
]
|
|
25420
25629
|
},
|
|
25421
25630
|
{
|
|
@@ -25497,7 +25706,15 @@ var init_intent_router = __esm({
|
|
|
25497
25706
|
// — the recordtype→profile grammar the forward templates above missed. Same
|
|
25498
25707
|
// recordTypeVisibilities modeling, answered from the profile side.
|
|
25499
25708
|
/\bwhich\s+record\s+types?\b.*\b(available|access|assigned)\s+to\b.*\bprofiles?\b/,
|
|
25500
|
-
/\b(available|access|assigned)\s+to\b.*\bprofiles?\b.*\brecord\s+types?\b
|
|
25709
|
+
/\b(available|access|assigned)\s+to\b.*\bprofiles?\b.*\brecord\s+types?\b/,
|
|
25710
|
+
// REACH (permissions/access cluster) — "why can't / won't <user> create /
|
|
25711
|
+
// pick / select the <X> RECORD TYPE" is a recordTypeVisibilities gap: the
|
|
25712
|
+
// container simply doesn't have that record type visible-for-create. The
|
|
25713
|
+
// literal "record type" / "recordtypeid" noun keeps this off why-cant-see
|
|
25714
|
+
// (record SHARING) and off object-access (object CRUD) — both of which
|
|
25715
|
+
// lack the record-type noun.
|
|
25716
|
+
/\bwhy\s+(?:can'?t|won'?t|cannot)\b[^.?!]{0,60}\b(?:create|pick|select|use|choose|see|open)\b[^.?!]{0,40}\brecord\s+type\b/,
|
|
25717
|
+
/\brecord\s?type\s?id\b[^.?!]{0,60}\b(?:pick|select|choose|create|use)\b/
|
|
25501
25718
|
]
|
|
25502
25719
|
},
|
|
25503
25720
|
{
|
|
@@ -25515,7 +25732,32 @@ var init_intent_router = __esm({
|
|
|
25515
25732
|
/\bwhat\s+permissions?\s+(does|do)\b.*\b(profiles?|permission\s+sets?|users?)\b/,
|
|
25516
25733
|
// Permission sets assigned to a named user (baseline-300 gap).
|
|
25517
25734
|
/\bpermission\s+sets?\b.*\bassigned\b.*\buser\b/,
|
|
25518
|
-
/\bwhat\s+permission\s+sets?\b.*\bassigned\b
|
|
25735
|
+
/\bwhat\s+permission\s+sets?\b.*\bassigned\b/,
|
|
25736
|
+
// REACH (permissions/access cluster): "does/can the <Named> profile /
|
|
25737
|
+
// <Named> perm set have|give|grant|read|edit|create|delete|access|see|
|
|
25738
|
+
// change <object>". These name a SPECIFIC granter (a profile/permission
|
|
25739
|
+
// set) and ask what access it confers — the exact effective_permissions
|
|
25740
|
+
// ask, which no earlier rule caught (the existing templates required the
|
|
25741
|
+
// literal "effective/combined access" or the generic word "permissions").
|
|
25742
|
+
// Anchored on the interrogative verb `does|can` PLUS the granter noun
|
|
25743
|
+
// (profile|perm set) PLUS an access verb, all clause-bounded (`[^.?!]`) so
|
|
25744
|
+
// one sentence's verb can't reach across into the next. The `(?<!\bwhy\s)`
|
|
25745
|
+
// lookbehind keeps "why can't the X profile see the record" on
|
|
25746
|
+
// why-cant-see; "who can …" field asks (no does/can-led granter) stay on
|
|
25747
|
+
// field-access; enumerative "which permission sets grant …" stays on
|
|
25748
|
+
// object-access (no does/can lead). recordtype-availability and
|
|
25749
|
+
// profile-security sit EARLIER, so a record-type / session-security
|
|
25750
|
+
// phrasing still wins by first-match. The leading `^(?!.*\blayouts?\b)`
|
|
25751
|
+
// yields any "which layout does the X profile SEE" question to
|
|
25752
|
+
// layout-access (a later rule) — "layout" anywhere disqualifies this
|
|
25753
|
+
// permission route.
|
|
25754
|
+
/^(?!.*\blayouts?\b).*?(?<!\bwhy\s)\b(?:does|can)\b[^.?!]{0,60}\b(?:profile|perm\s*sets?|permission\s+sets?)\b[^.?!]{0,60}\b(?:have|give|gives?|grant|grants?|read|edit|create|delete|access|see|change)\b/,
|
|
25755
|
+
// "which perm sets are stacked on top of / assigned on top of the <Named>
|
|
25756
|
+
// profile" — a union-of-containers ask (what the stack effectively grants).
|
|
25757
|
+
/\bwhich\s+perm\s*sets?\b[^.?!]{0,40}\b(?:stacked|stack|on\s+top\s+of|added\s+to|layered)\b/,
|
|
25758
|
+
// "for <PermSetA>, <PermSetB>, <PermSetC> perm sets, what does each
|
|
25759
|
+
// contribute" — the division-of-access breakdown across a bundle.
|
|
25760
|
+
/\bperm\s*sets?\b[^.?!]{0,20},[^.?!]{0,80}\bwhat\s+does\s+each\s+(?:contribute|grant|add|allow)\b/
|
|
25519
25761
|
]
|
|
25520
25762
|
},
|
|
25521
25763
|
{
|
|
@@ -25578,7 +25820,16 @@ var init_intent_router = __esm({
|
|
|
25578
25820
|
reason: "What happens when {Object}.{field} becomes {value} \u2014 the automation coupled to a value/stage transition, from the vault (lifecycle_process).",
|
|
25579
25821
|
patterns: [
|
|
25580
25822
|
/\bwhat\s+happens\s+when\b.*\b(becomes?|turns?|changes?\s+to|is\s+set\s+to|reaches?)\b/,
|
|
25581
|
-
|
|
25823
|
+
// Up to three optional adverb words between the verb and "when" — "what
|
|
25824
|
+
// runs AUTOMATICALLY when a Lead is converted" / "what fires IN THE
|
|
25825
|
+
// BACKGROUND when …" were unrouted (eval lifecycle family). The DML-event
|
|
25826
|
+
// save-order rule stays disjoint: its verb list has no transition verbs
|
|
25827
|
+
// (converted / closed won / approved), so nothing is stolen either way.
|
|
25828
|
+
/\bwhat\s+(?:happens|runs|fires)\b(?:\s+\w+){0,3}\s+when\b.*\b(closed\s+won|closed\s+lost|converted|approved|activated)\b/,
|
|
25829
|
+
// Nominalized transition — "what runs ON Lead CONVERSION?" / "what fires
|
|
25830
|
+
// upon Case escalation to closed" has no "when …is converted" clause at
|
|
25831
|
+
// all; the nominal "on/upon <Entity> conversion" form routes the same.
|
|
25832
|
+
/\bwhat\s+(?:happens|runs|fires|occurs|triggers)\b[^.?!]{0,40}\b(?:on|upon|during|after)\s+(?:an?\s+|the\s+)?\w+\s+conversion\b/,
|
|
25582
25833
|
// P1e — generic state-transition verbs beyond the Opportunity/Lead
|
|
25583
25834
|
// hardcoded list: "submitted", "disqualified", "completed", "enrolled" are
|
|
25584
25835
|
// common transitions on other objects (Applications, Enrollments,
|
|
@@ -25607,7 +25858,21 @@ var init_intent_router = __esm({
|
|
|
25607
25858
|
/\bwho\s+can\s+(see|access|view|edit|read)\b.*\b(all|every)\b.*\brecords?\b/,
|
|
25608
25859
|
/\b(which|what)\s+(profiles?|permission\s+sets?|roles?|groups?)\b.*\b(see|access|edit)\b.*\brecords?\b/,
|
|
25609
25860
|
// Object-level access without the word "records" — must beat field-access.
|
|
25610
|
-
/\bwho\s+can\s+access\b(?!.*\bfield\b)
|
|
25861
|
+
/\bwho\s+can\s+access\b(?!.*\bfield\b)/,
|
|
25862
|
+
// Enumerative SINGULAR phrasings — "list every profile with delete
|
|
25863
|
+
// permission on Contact", "show me every profile that can access Case".
|
|
25864
|
+
// The bare noun "profile" is an intent signal here, not a named entity;
|
|
25865
|
+
// these fell through to the generic schema list rule (eval family A).
|
|
25866
|
+
/\b(?:list|show)\b.*\bevery\s+profiles?\b.*\b(?:permission|access)/,
|
|
25867
|
+
/\bevery\s+profiles?\s+(?:that|who|with)\b.*\b(?:access|see|view|edit|read|delete|create)\b/,
|
|
25868
|
+
/\b(?:which|what)\s+profiles?\b.*\b(?:create|read|edit|delete|view)\s+permission\b/,
|
|
25869
|
+
// REACH (permissions/access cluster): "is <Object__c> visible / accessible
|
|
25870
|
+
// to the <Named> profile / perm set / role" — the forward object-record
|
|
25871
|
+
// access ask, which who_can_access_object answers (the agent reads whether
|
|
25872
|
+
// that container is among the granters). `__c`-anchored + "visible/…
|
|
25873
|
+
// to <container>" so it never grabs a layout ("is Account.Name visible on
|
|
25874
|
+
// the layout") or a schema ("is Payment__c an object") question.
|
|
25875
|
+
/\bis\b[^.?!]{0,20}\b\w+__c\b[^.?!]{0,20}\b(?:visible|accessible|available|readable|editable)\b[^.?!]{0,20}\bto\b[^.?!]{0,30}\b(?:profile|perm\s*sets?|permission\s+sets?|role|user)\b/
|
|
25611
25876
|
]
|
|
25612
25877
|
},
|
|
25613
25878
|
{
|
|
@@ -25620,7 +25885,20 @@ var init_intent_router = __esm({
|
|
|
25620
25885
|
patterns: [
|
|
25621
25886
|
/\bwhy\s+(can'?t|cannot|can\s+not)\b.*\b(see|view|access)\b.*\b(record|account|case|contact|lead|opportunity)\b/,
|
|
25622
25887
|
/\bcan'?t\s+(see|view|access)\b.*\b(record|account|case|opportunity|contact|lead)\b/,
|
|
25623
|
-
/\bwhy\s+(can'?t|cannot|can\s+not)\b.*\b(see|view|access)\b.*\b(an?\s+)?(account|case|contact|lead|opportunity)\b
|
|
25888
|
+
/\bwhy\s+(can'?t|cannot|can\s+not)\b.*\b(see|view|access)\b.*\b(an?\s+)?(account|case|contact|lead|opportunity)\b/,
|
|
25889
|
+
// REACH (permissions/access cluster): the existing templates used a
|
|
25890
|
+
// SINGULAR `\brecord\b` and only see/view/access, so a plural "records"
|
|
25891
|
+
// ask ("why can't the Manager ROLE see Enrollment RECORDS", "why can't a
|
|
25892
|
+
// user EDIT Order__c RECORDS") fell through. Add plural
|
|
25893
|
+
// `records?` + the `edit|read` verbs — this is still the record-sharing
|
|
25894
|
+
// cascade (OWD → sharing → role hierarchy), the honest tool for a
|
|
25895
|
+
// "why can't X see/edit these RECORDS" question. The literal "records"
|
|
25896
|
+
// keeps it OFF field-access (a named FIELD, no "records").
|
|
25897
|
+
/\bwhy\s+(?:can'?t|won'?t|cannot|can\s+not)\b[^.?!]{0,60}\b(?:see|view|access|edit|read)\b[^.?!]{0,60}\brecords?\b/,
|
|
25898
|
+
// Negative-contrast visibility — "why can a <user> see A records BUT NOT
|
|
25899
|
+
// B" — the same sharing-cascade question phrased as a see-one-not-the-other
|
|
25900
|
+
// puzzle. The "but not" tail distinguishes it from a plain who-can-see.
|
|
25901
|
+
/\bwhy\s+can\b[^.?!]{0,60}\b(?:see|view|access)\b[^.?!]{0,60}\brecords?\b[^.?!]{0,40}\bbut\s+not\b/
|
|
25624
25902
|
]
|
|
25625
25903
|
},
|
|
25626
25904
|
{
|
|
@@ -25743,7 +26021,41 @@ var init_intent_router = __esm({
|
|
|
25743
26021
|
// "field access audit for Email" / "field access for X" — "field access"
|
|
25744
26022
|
// with the field named after (the (field|object)-after-access patterns
|
|
25745
26023
|
// missed it). Battery gap.
|
|
25746
|
-
/\bfield[-\s]access\b
|
|
26024
|
+
/\bfield[-\s]access\b/,
|
|
26025
|
+
// Eval family C — qualifier hijack. "which fields are only ever written
|
|
26026
|
+
// BY an integration user" is a field WRITE-access audit (who can edit),
|
|
26027
|
+
// not integration topology: the "integration" qualifier was dragging it
|
|
26028
|
+
// onto integration_map.
|
|
26029
|
+
/\b(?:which|what)\s+fields?\b[^.?!]{0,80}\b(?:written|edited|updated|writable)\b[^.?!]{0,40}\bby\b/,
|
|
26030
|
+
// "compliance report … who touches the <X> field" — who-touches-a-field
|
|
26031
|
+
// is FLS edit access, not report run-history (the "report" qualifier was
|
|
26032
|
+
// dragging it onto reports-usage).
|
|
26033
|
+
/\bwho\s+touch(?:es)?\b[^.?!]{0,60}\b(?:fields?\b|__c\b)/,
|
|
26034
|
+
// REACH (permissions/access cluster) — FLS on a NAMED field:
|
|
26035
|
+
// (a) "who [adverb] can see/read/view/edit/access <Field__c or
|
|
26036
|
+
// Object.field>" — the existing who-can-see template broke on an
|
|
26037
|
+
// adverb ("who can ACTUALLY see Some_Field__c"). Field-anchored
|
|
26038
|
+
// (dotted or `__c`) and clause-guarded against "records" so a
|
|
26039
|
+
// who-can-see-RECORDS ask stays on who-can-access-object.
|
|
26040
|
+
/\bwho\s+can\b(?![^.?!]*\brecords?\b)[^.?!]{0,25}\b(?:see|read|view|edit|access)\b[^.?!]{0,40}(?:\b\w+\.\w+\b|\b\w+__c\b)/,
|
|
26041
|
+
// (b) "can/does <someone> edit/see <Object.field>" — FLS on a DOTTED
|
|
26042
|
+
// field ref ("Can Analytics edit Opportunity.Amount"). Restricted to a
|
|
26043
|
+
// dotted ref (or a bare `__c` accompanied by the word "field") so a
|
|
26044
|
+
// bare `__c` OBJECT ("can a user see Payment__c?") is NOT mistaken for
|
|
26045
|
+
// a field; the `(?<!\bwhy\s)` and no-"records" guards keep why-cant /
|
|
26046
|
+
// record asks off this rule, and effective-permissions (earlier) still
|
|
26047
|
+
// wins any granter-worded "does the X profile …" phrasing.
|
|
26048
|
+
/(?<!\bwhy\s)\b(?:can|does)\b(?![^.?!]*\brecords?\b)[^.?!]{0,40}\b(?:see|read|view|edit|access)\b[^.?!]{0,30}(?:\b\w+\.\w+\b|\b\w+__c\b[^.?!]{0,25}\bfields?\b|\bfields?\b[^.?!]{0,25}\b\w+__c\b)/,
|
|
26049
|
+
// (c) "why can't <someone> edit/see <Object.field or Field__c>" — an FLS
|
|
26050
|
+
// gap on a named FIELD (no "records"), which why_cant_user_see_record
|
|
26051
|
+
// (record sharing) does not answer; field_access_audit shows who holds
|
|
26052
|
+
// read/edit on the field. The no-"records" guard routes the RECORD
|
|
26053
|
+
// variant ("why can't X see <Object> records") to why-cant-see instead.
|
|
26054
|
+
// "access" is deliberately EXCLUDED from the verb list here — the noun
|
|
26055
|
+
// phrase "<managed-package> access" (in a "why can't they run the
|
|
26056
|
+
// managed-package action" question) would otherwise be misread as an
|
|
26057
|
+
// FLS verb.
|
|
26058
|
+
/\bwhy\s+can'?t\b(?![^.?!]*\brecords?\b)[^.?!]{0,40}\b(?:see|read|view|edit)\b[^.?!]{0,40}(?:\b\w+\.\w+\b|\b\w+__c\b)/
|
|
25747
26059
|
]
|
|
25748
26060
|
},
|
|
25749
26061
|
{
|
|
@@ -25763,6 +26075,27 @@ var init_intent_router = __esm({
|
|
|
25763
26075
|
/\bdoes\b.*\bapex\b.*\b(enforce|CRUD|FLS)\b/
|
|
25764
26076
|
]
|
|
25765
26077
|
},
|
|
26078
|
+
{
|
|
26079
|
+
// Deactivate-a-permission-set what-if (RESIDUAL 2). There is NO dedicated
|
|
26080
|
+
// what_if_* simulator for permission sets in the vault tier, so this is an
|
|
26081
|
+
// HONEST route to permission_risk_report (+ its impact edges via get_impact
|
|
26082
|
+
// once the set is resolved): the report surfaces what access the set grants
|
|
26083
|
+
// and who depends on it — the closest truthful answer to "does anything
|
|
26084
|
+
// break if we deactivate it". needsResolve so the named permission set is
|
|
26085
|
+
// resolved first. Sits before over-permission so a deactivation ask beats
|
|
26086
|
+
// the generic god-mode phrasing; requires the deactivate/turn-off verb + a
|
|
26087
|
+
// permission-set noun, so it never steals a plain over-privilege question.
|
|
26088
|
+
intent: "permission-set-deactivation-impact",
|
|
26089
|
+
plane: "vault",
|
|
26090
|
+
tools: ["sfi.resolve", "sfi.permission_risk_report", "sfi.get_impact"],
|
|
26091
|
+
liveRequired: false,
|
|
26092
|
+
needsResolve: true,
|
|
26093
|
+
reason: "No what_if simulator exists for permission sets, so this routes honestly to permission_risk_report (what the set grants + who depends on it) with get_impact for the dependency surface \u2014 the truthful stand-in for a deactivation blast radius, not a fabricated simulation.",
|
|
26094
|
+
patterns: [
|
|
26095
|
+
/\b(?:deactivat\w+|disabl\w+|turn(?:ed|ing)?\s+off|remov\w+|delet\w+)\b[^.?!]{0,60}\bpermission\s+sets?\b/,
|
|
26096
|
+
/\bpermission\s+sets?\b[^.?!]{0,60}\b(?:is|are|was|were|gets?|being)\s+(?:deactivated|disabled|turned\s+off|removed|deleted)\b/
|
|
26097
|
+
]
|
|
26098
|
+
},
|
|
25766
26099
|
{
|
|
25767
26100
|
intent: "over-permission",
|
|
25768
26101
|
plane: "vault",
|
|
@@ -25788,6 +26121,36 @@ var init_intent_router = __esm({
|
|
|
25788
26121
|
/\bpermission\s+sets?\b.*\binstead\s+of\b.*\bprofiles?\b/
|
|
25789
26122
|
]
|
|
25790
26123
|
},
|
|
26124
|
+
{
|
|
26125
|
+
// Profile login/session security — IP ranges ("IP relaxation"), login
|
|
26126
|
+
// hours, session settings (sfi.profile_security). Eval family C: the
|
|
26127
|
+
// "integration users" qualifier in "do any profiles have IP relaxation
|
|
26128
|
+
// that would block integration users" dragged this onto integration_map;
|
|
26129
|
+
// the head noun is the PROFILE security posture. Enumerative asks list
|
|
26130
|
+
// Profiles then drill per profile (profile_security requires a profileId).
|
|
26131
|
+
intent: "profile-security",
|
|
26132
|
+
plane: "vault",
|
|
26133
|
+
tools: ["sfi.list_components", "sfi.profile_security"],
|
|
26134
|
+
liveRequired: false,
|
|
26135
|
+
needsResolve: false,
|
|
26136
|
+
reason: "Profile login/session security (login IP ranges \u2014 'IP relaxation' \u2014 login hours, org session settings) is declared Profile metadata: list_components(type Profile) enumerates, then profile_security per profile reads the posture.",
|
|
26137
|
+
suggestArgs: () => ({ type: "Profile" }),
|
|
26138
|
+
patterns: [
|
|
26139
|
+
/\bip\s+(?:relaxation|relaxed|ranges?|restrictions?|whitelists?|allowlists?)\b/,
|
|
26140
|
+
/\blogin\s+(?:ip|hours?)\b/,
|
|
26141
|
+
/\bprofiles?\b[^.?!]{0,50}\b(?:session\s+(?:timeout|settings?)|login\s+restrictions?)\b/,
|
|
26142
|
+
// REACH (permissions/access cluster): MFA / password-policy / session
|
|
26143
|
+
// security compared ACROSS profiles ("which profiles have MFA or session
|
|
26144
|
+
// security settings weaker than the rest", "what password policies and
|
|
26145
|
+
// session timeout are set per profile"). Both word orders (profile→setting
|
|
26146
|
+
// and setting→profile). These are Profile login/session posture — the
|
|
26147
|
+
// profile_security surface — which the IP/login-hours templates above did
|
|
26148
|
+
// not cover. The `\bprofiles?\b` co-anchor keeps a generic "what is MFA"
|
|
26149
|
+
// knowledge question on guidance.
|
|
26150
|
+
/\bprofiles?\b[^.?!]{0,60}\b(?:mfa|multi[-\s]?factor|password\s+polic\w*|session\s+(?:security|timeout|settings?))\b/,
|
|
26151
|
+
/\b(?:mfa|multi[-\s]?factor|password\s+polic\w*|session\s+(?:security|timeout|settings?))\b[^.?!]{0,60}\bprofiles?\b/
|
|
26152
|
+
]
|
|
26153
|
+
},
|
|
25791
26154
|
{
|
|
25792
26155
|
// EARLY PRECISION RULE (P14-ROUTER-safe-delete-misroute): a long
|
|
25793
26156
|
// compound delete-verdict question enumerates nouns ("every layout,
|
|
@@ -25808,7 +26171,13 @@ var init_intent_router = __esm({
|
|
|
25808
26171
|
patterns: [
|
|
25809
26172
|
/\bsafe(?:[\s_-]+to[\s_-]+delete|_to_delete_field)\b/,
|
|
25810
26173
|
/\b(block|prevent)\w*\b[^.?!]{0,30}\bdeletion\b/,
|
|
25811
|
-
/\bbefore\s+deleting\b
|
|
26174
|
+
/\bbefore\s+deleting\b/,
|
|
26175
|
+
// "can I SAFELY delete X, Y, Z or are they referenced somewhere" — the
|
|
26176
|
+
// adverb "safely" sits between "can i" and "delete", so the later
|
|
26177
|
+
// `can\s+i\s+delete` pattern misses it. The safe/safely + delete/remove
|
|
26178
|
+
// frame is the honest safe_to_delete_field ask (FIELD-FORENSICS REACH).
|
|
26179
|
+
/\bcan\s+i\s+safely\s+(delete|remove)\b/,
|
|
26180
|
+
/\bsafely\s+(delete|remove)\b[^.?!]{0,80}\breferenced\b/
|
|
25812
26181
|
]
|
|
25813
26182
|
},
|
|
25814
26183
|
{
|
|
@@ -25856,6 +26225,32 @@ var init_intent_router = __esm({
|
|
|
25856
26225
|
/\bpermission\s+set\s+groups?\b[^.?!]{0,40}\b(no\s+one|nobody|unassigned|unused|assigned\s+to\s+nobody)\b/
|
|
25857
26226
|
]
|
|
25858
26227
|
},
|
|
26228
|
+
{
|
|
26229
|
+
// HONEST GAP (eval family D): "which USERS have permission set X" is a
|
|
26230
|
+
// holder ROSTER — PermissionSetAssignment is runtime assignment data the
|
|
26231
|
+
// vault does not model, and no live tool answers it yet.
|
|
26232
|
+
// effective_permissions / object_access_audit describe what a permission
|
|
26233
|
+
// set GRANTS, not who HOLDS it, so substituting them would be confidently
|
|
26234
|
+
// wrong. Mirrors the unassigned-permset-groups gap above. The reverse
|
|
26235
|
+
// direction ("what permission sets does user X have") stays on
|
|
26236
|
+
// effective-permissions via first-match.
|
|
26237
|
+
intent: "permset-user-roster",
|
|
26238
|
+
plane: "vault",
|
|
26239
|
+
tools: [],
|
|
26240
|
+
liveRequired: false,
|
|
26241
|
+
needsResolve: false,
|
|
26242
|
+
reason: "Which users hold a permission set (PermissionSetAssignment) is not modeled \u2014 capability gap.",
|
|
26243
|
+
gap: {
|
|
26244
|
+
category: "permset-user-roster",
|
|
26245
|
+
note: 'PermissionSetAssignment modeling is not built yet, so "which users have permission set X" cannot be answered. Do not substitute effective_permissions or object_access_audit \u2014 they describe what a permission set GRANTS, not who holds it.'
|
|
26246
|
+
},
|
|
26247
|
+
patterns: [
|
|
26248
|
+
/\b(which|what|list)\s+users?\b[^.?!]{0,40}\b(have|hold|with|assigned)\b[^.?!]{0,40}\bpermission\s+sets?\b/,
|
|
26249
|
+
/\bwho\s+(has|holds|is\s+assigned)\b[^.?!]{0,50}\bpermission\s+sets?\b/,
|
|
26250
|
+
/\b(everyone|everybody|all\s+users?)\b[^.?!]{0,30}\bwith\b[^.?!]{0,40}\bpermission\s+sets?\b/,
|
|
26251
|
+
/\busers?\b[^.?!]{0,30}\bassigned\b[^.?!]{0,30}\bpermission\s+sets?\b/
|
|
26252
|
+
]
|
|
26253
|
+
},
|
|
25859
26254
|
{
|
|
25860
26255
|
intent: "empty-queues-groups",
|
|
25861
26256
|
plane: "vault",
|
|
@@ -25868,7 +26263,39 @@ var init_intent_router = __esm({
|
|
|
25868
26263
|
/\b(queues?|public\s+groups?)\b.*\b(empty|no\s+members?|unused)\b/,
|
|
25869
26264
|
/\b(which|what)\s+queues?\b.*\b(set\s+up|for|exist)\b/,
|
|
25870
26265
|
/\bpublic\s+groups?\b.*\b(exist|who\s+is)\b/,
|
|
25871
|
-
/\bwhat\s+public\s+groups?\b
|
|
26266
|
+
/\bwhat\s+public\s+groups?\b/,
|
|
26267
|
+
// DISCOVERY/META REACH: routing-trap SYMPTOM questions. When work "isn't
|
|
26268
|
+
// getting picked up" / members "can't see cases routed to them", the
|
|
26269
|
+
// HONEST first check is whether the queue actually HAS members — exactly
|
|
26270
|
+
// what empty_queues_and_groups reports (memberCount / unknownMemberCount).
|
|
26271
|
+
// Every pattern REQUIRES the literal "queue(s)" AND a NEGATIVE/failure
|
|
26272
|
+
// frame (can't / not / isn't picked up / sitting in / exist-challenge),
|
|
26273
|
+
// so a neutral "which queues does X route to and who are the members"
|
|
26274
|
+
// stays a get_component ask (handled by the queue-membership rule below),
|
|
26275
|
+
// and a record-sharing ("why can't X see an Account") question — which
|
|
26276
|
+
// carries no "queue" — never lands here.
|
|
26277
|
+
// "why can (members of) <queue> NOT see … routed to them"
|
|
26278
|
+
new RegExp(`\\bqueues?\\b[^.?!]{0,80}\\b(?:can'?t|cannot|not\\s+(?:see|get|pick|able)|isn'?t|aren'?t)\\b`),
|
|
26279
|
+
// Queue named only by its `_Queue` API-name suffix (no standalone "queue"
|
|
26280
|
+
// word — underscore is a word char so `\bqueue\b` misses "ada_team_queue")
|
|
26281
|
+
// WITH a member/routing symptom. "why can members of <X>_Queue not see …
|
|
26282
|
+
// cases ROUTED to them" — the routing-trap membership check.
|
|
26283
|
+
// Failure-framed only ("NOT see", "can't", "isn't picked up") — a neutral
|
|
26284
|
+
// "which queues does <X>_Queue ROUTE to and who are the members" stays a
|
|
26285
|
+
// get_component ask (route/routed deliberately excluded here).
|
|
26286
|
+
/_queue\b[^.?!]{0,80}\b(?:not\s+(?:see|get|pick|able)|can'?t|cannot|isn'?t\s+(?:getting|picked))\b/,
|
|
26287
|
+
/\bmembers?\s+of\b[^.?!]{0,30}_queue\b[^.?!]{0,80}\b(?:not|can'?t|cannot)\b/,
|
|
26288
|
+
new RegExp(`\\b(?:can'?t|cannot|not\\s+(?:see|get|pick|able)|isn'?t|aren'?t)\\b[^.?!]{0,80}\\bqueues?\\b`),
|
|
26289
|
+
// "why is the Lead SITTING IN <queue> instead of getting PICKED UP —
|
|
26290
|
+
// queue members exist, right?" — the stuck-in-queue symptom.
|
|
26291
|
+
/\bsitting\s+in\b[^.?!]{0,40}\bqueues?\b/,
|
|
26292
|
+
/\bqueues?\b[^.?!]{0,40}\b(?:picked\s+up|getting\s+picked)\b/,
|
|
26293
|
+
/\bqueue\s+members?\b[^.?!]{0,30}\bexist\b/,
|
|
26294
|
+
// "why can't a user REASSIGN a Case to <Named>? They can touch every
|
|
26295
|
+
// other QUEUE" — reassignment-to-a-queue trouble; the membership /
|
|
26296
|
+
// queue-access check is the honest first probe. Requires a can't/cannot
|
|
26297
|
+
// failure frame co-occurring with "reassign" and "queue".
|
|
26298
|
+
new RegExp(`\\b(?:can'?t|cannot)\\b[^.?!]{0,40}\\breassign\\w*\\b.*\\bqueues?\\b`)
|
|
25872
26299
|
]
|
|
25873
26300
|
},
|
|
25874
26301
|
{
|
|
@@ -25921,7 +26348,12 @@ var init_intent_router = __esm({
|
|
|
25921
26348
|
reason: "Profile\u2192permission-set migration / merge-split planning from permission metadata.",
|
|
25922
26349
|
patterns: [
|
|
25923
26350
|
/\bprofiles?\b.*\b(to|into|vs)\b.*\bpermission\s+sets?\b/,
|
|
25924
|
-
|
|
26351
|
+
// `\bmerge\b` never matched the PAST tense "merged" (no boundary after
|
|
26352
|
+
// "merge"), so "what would break if I MERGED the A and B profiles" fell
|
|
26353
|
+
// through to unrouted. `merg\w*` catches merge/merged/merging; the two
|
|
26354
|
+
// named profiles + the plural "profiles" keep it precise (USAGE/impact
|
|
26355
|
+
// REACH — profiles/access family).
|
|
26356
|
+
/\b(merg\w*|split|splitting|consolidat\w*)\b.*\bprofiles?\b/,
|
|
25925
26357
|
/\bprofile\s+migration\b/,
|
|
25926
26358
|
// PASSIVE voice (P1c): "what if two profiles are merged / were consolidated"
|
|
25927
26359
|
// — the active templates above ("if I merge profiles") missed the passive
|
|
@@ -25939,11 +26371,20 @@ var init_intent_router = __esm({
|
|
|
25939
26371
|
liveRequired: false,
|
|
25940
26372
|
needsResolve: true,
|
|
25941
26373
|
reason: "Traces where a field flows (upstream/downstream) across Apex, flows, integrations.",
|
|
26374
|
+
// DEMOTED from catch-all (eval family E): field_lineage was the default
|
|
26375
|
+
// for any field-adjacent question carrying "flow(s)" or a bare
|
|
26376
|
+
// "upstream/downstream". Every pattern now requires an explicit
|
|
26377
|
+
// lineage/provenance/movement frame, so the earlier field-access /
|
|
26378
|
+
// explain-flow / save-order rules keep their heads and only genuine
|
|
26379
|
+
// "where does this data come from / flow to" questions land here.
|
|
25942
26380
|
patterns: [
|
|
25943
26381
|
/\b(data\s+flow|lineage)\b/,
|
|
25944
|
-
/\bwhere\s+does\b.*\b(field|data|pii|it)\b.*\b(flow|go)\b/,
|
|
25945
|
-
/\bfield\b
|
|
25946
|
-
|
|
26382
|
+
/\bwhere\s+does\b.*\b(field|data|pii|it)\b.*\b(flow|go|come\s+from)\b/,
|
|
26383
|
+
/\bfield\b[^.?!]{0,50}\bflows?\s+(?:to|into|out|through|downstream|between)\b/,
|
|
26384
|
+
// A change/delete/disable verb marks an IMPACT question, not lineage —
|
|
26385
|
+
// "what breaks downstream if I delete the X field" stays impact-analysis.
|
|
26386
|
+
/^(?!.*\b(?:break|delet|disabl|deactivat|remov|chang)\w*\b).*\b(upstream|downstream)\b[^.?!]{0,40}\b(?:fields?|data)\b/,
|
|
26387
|
+
/^(?!.*\b(?:break|delet|disabl|deactivat|remov|chang)\w*\b).*\b(?:fields?|data)\b[^.?!]{0,40}\b(upstream|downstream)\b/
|
|
25947
26388
|
]
|
|
25948
26389
|
},
|
|
25949
26390
|
{
|
|
@@ -26000,6 +26441,68 @@ var init_intent_router = __esm({
|
|
|
26000
26441
|
]
|
|
26001
26442
|
},
|
|
26002
26443
|
// === Automation / order-of-execution (vault) ==============================
|
|
26444
|
+
{
|
|
26445
|
+
// Disable-a-trigger what-if (eval family C): "blast radius if I disable
|
|
26446
|
+
// trigger T" was hijacked by qualifier words — "integration"/vendor-sync
|
|
26447
|
+
// onto integration_map, bare "downstream" onto field_lineage — when the
|
|
26448
|
+
// head question is the dedicated what_if_disable_trigger simulation.
|
|
26449
|
+
// Sits FIRST in the automation cluster so a disable ask beats the generic
|
|
26450
|
+
// trigger-order/automation-on-object phrasings; those carry no
|
|
26451
|
+
// disable/turn-off verb, so nothing is stolen from them.
|
|
26452
|
+
intent: "what-if-disable-trigger",
|
|
26453
|
+
plane: "vault",
|
|
26454
|
+
tools: ["sfi.resolve", "sfi.what_if_disable_trigger"],
|
|
26455
|
+
liveRequired: false,
|
|
26456
|
+
needsResolve: true,
|
|
26457
|
+
reason: "What stops firing / breaks if a named trigger is disabled \u2014 the dedicated what_if_disable_trigger simulation over the vault graph (not integration topology, not generic lineage).",
|
|
26458
|
+
patterns: [
|
|
26459
|
+
/\bdisabl\w+\b[^.?!]{0,40}\btriggers?\b/,
|
|
26460
|
+
/\btriggers?\b[^.?!]{0,40}\b(?:is|was|were|gets?|being)\s+disabled\b/,
|
|
26461
|
+
/\b(?:turn(?:ed|ing)?\s+off|switch(?:ed|ing)?\s+off)\b[^.?!]{0,40}\btriggers?\b/
|
|
26462
|
+
]
|
|
26463
|
+
},
|
|
26464
|
+
{
|
|
26465
|
+
// Deactivate-a-flow what-if (RESIDUAL 2): "what breaks if I deactivate the
|
|
26466
|
+
// Onboarding flow" / "if I turn off FlowA and FlowB, does anything break".
|
|
26467
|
+
// The dedicated what_if_deactivate_flow simulator owns these — before it,
|
|
26468
|
+
// the deactivate-flow phrasing fell to generic impact-analysis (get_impact,
|
|
26469
|
+
// not the flow-specific simulator) and the "turn off … does anything break"
|
|
26470
|
+
// shape went unrouted entirely. Sits with the disable-trigger rule ahead of
|
|
26471
|
+
// trigger-order/automation-on-object: those carry no deactivate/turn-off
|
|
26472
|
+
// verb, so nothing is stolen from a save-order question. The deactivate/
|
|
26473
|
+
// turn-off verb + FLOW noun is the discriminator; needsResolve so the named
|
|
26474
|
+
// flow (or the first of several) is resolved before the simulation.
|
|
26475
|
+
intent: "what-if-deactivate-flow",
|
|
26476
|
+
plane: "vault",
|
|
26477
|
+
tools: ["sfi.resolve", "sfi.what_if_deactivate_flow"],
|
|
26478
|
+
liveRequired: false,
|
|
26479
|
+
needsResolve: true,
|
|
26480
|
+
reason: "What stops running / breaks if a named flow is deactivated \u2014 the dedicated what_if_deactivate_flow simulation over the vault graph (not a generic get_impact walk).",
|
|
26481
|
+
patterns: [
|
|
26482
|
+
/\b(?:deactivat\w+|disabl\w+|turn(?:ed|ing)?\s+off|switch(?:ed|ing)?\s+off)\b[^.?!]{0,60}\bflows?\b/,
|
|
26483
|
+
/\bflows?\b[^.?!]{0,60}\b(?:is|are|was|were|gets?|being)\s+(?:deactivated|disabled|turned\s+off)\b/,
|
|
26484
|
+
/\bflows?\b[^.?!]{0,60}\b(?:deactivat\w+|turn(?:ed|ing)?\s+off)\b/,
|
|
26485
|
+
// API-name form: "turn off Discount_Flow and Pricing_Flow, does anything
|
|
26486
|
+
// break" — the flow names carry the `_Flow` suffix rather than a standalone
|
|
26487
|
+
// "flow" word, so anchor on a deactivate/turn-off verb next to a *_Flow
|
|
26488
|
+
// component name (the whole-word "flow" alternations above miss this).
|
|
26489
|
+
/\b(?:deactivat\w+|disabl\w+|turn(?:ed|ing)?\s+off|switch(?:ed|ing)?\s+off)\b[^.?!]{0,40}\b[A-Za-z][A-Za-z0-9]*_Flow\b/i,
|
|
26490
|
+
// Flow-family API-name suffixes beyond `_Flow` (flow-family REACH): a
|
|
26491
|
+
// deactivate/turn-off verb next to a `_Process` / `_Orch` / `_Screen_Flow`
|
|
26492
|
+
// component name (e.g. Application_Save_RT_Orch). The SINGULAR flow-family
|
|
26493
|
+
// suffix (`_flow`, not the plural `_flows`) keeps the existing guard that a
|
|
26494
|
+
// plural embedded-Flow name like `ADA_Accom_Flow_Attribute_Flows` stays on
|
|
26495
|
+
// impact-analysis.
|
|
26496
|
+
/\b(?:deactivat\w+|disabl\w+|turn(?:ed|ing)?\s+off|switch(?:ed|ing)?\s+off)\b[^.?!]{0,60}\b[a-z][a-z0-9_]*_(?:process|orch|screen_flow)\b/i,
|
|
26497
|
+
// "what would happen if I deactivated <flow-suffix name>" — the explicit
|
|
26498
|
+
// what-if frame with a flow-family-named component.
|
|
26499
|
+
/\bwhat\s+would\s+happen\s+if\s+i\s+deactivat\w+\b[^.?!]{0,60}\b[a-z][a-z0-9_]*_(?:flow|process|orch|screen_flow)\b/i,
|
|
26500
|
+
// "suppose we deactivated <NamedComponent> versus <NamedComponent>" — the
|
|
26501
|
+
// "suppose … deactivated" lead is absent from the "what if we deactivate"
|
|
26502
|
+
// impact-analysis guard, so a >=2-underscore API name is safe here.
|
|
26503
|
+
new RegExp(`\\bsuppose\\s+we\\s+deactivat\\w+\\b[^.?!]{0,60}\\b${NAMED_COMPONENT_ID}\\b`)
|
|
26504
|
+
]
|
|
26505
|
+
},
|
|
26003
26506
|
{
|
|
26004
26507
|
intent: "trigger-order",
|
|
26005
26508
|
plane: "vault",
|
|
@@ -26016,7 +26519,9 @@ var init_intent_router = __esm({
|
|
|
26016
26519
|
},
|
|
26017
26520
|
patterns: [
|
|
26018
26521
|
/\b(trigger\s+order|order\s+of\s+execution)\b/,
|
|
26019
|
-
|
|
26522
|
+
// One optional adverb between "what" and the verb — "what ACTUALLY
|
|
26523
|
+
// happens on save" fell through to unrouted (eval OVER-CLARIFY family A).
|
|
26524
|
+
/\bwhat\s+(?:\w+\s+)?(happens|runs|fires)\b.*\b(on\s+save|when\b.*\b(created|saved|updated|inserted|deleted|undeleted|restored))\b/,
|
|
26020
26525
|
// "what happens when I update Contact" — present-tense DML without "on save"
|
|
26021
26526
|
/\bwhat\s+(happens|runs|fires)\b.*\bwhen\b.*\b(i\s+)?(update|insert|delete|save|create|edit)\b/,
|
|
26022
26527
|
// "which/what flows|triggers|VRs|workflows run|fire when ..." — the
|
|
@@ -26030,8 +26535,14 @@ var init_intent_router = __esm({
|
|
|
26030
26535
|
// "what happens when a Case STATUS CHANGES" — a status/stage change is a
|
|
26031
26536
|
// save-order event the "(created|updated|...)" verb list missed (B21).
|
|
26032
26537
|
/\bwhat\s+(happens|runs|fires)\b.*\b(status|stage)\b.*\bchang/,
|
|
26033
|
-
// "What runs on Account insert?" — DML event without "on save" / "when"
|
|
26034
|
-
|
|
26538
|
+
// "What runs on Account insert?" — DML event without "on save" / "when"
|
|
26539
|
+
// (B21). Verb-symmetric "fires"/"happens" too: "what FIRES on X insert
|
|
26540
|
+
// during the integration load — what runs bulk" is a save-order head
|
|
26541
|
+
// question; the bulk/load/integration qualifiers must not drag it onto
|
|
26542
|
+
// governor_limit_risks or integration_map (eval family C).
|
|
26543
|
+
// ("what happens IF I delete…" is an impact/what-if frame, not a DML
|
|
26544
|
+
// save-order ask — the immediate "if" is excluded so it falls through.)
|
|
26545
|
+
/\bwhat\s+(?:runs|fires|happens)\b(?!\s+if\b).*\b(insert|update|delete|undelete)\b/,
|
|
26035
26546
|
// "what happens when I SAVE an Evaluation" — present-tense "save"/"saves"/
|
|
26036
26547
|
// "saving" (the verb list above only had past-tense "saved"). Question-
|
|
26037
26548
|
// battery gap.
|
|
@@ -26040,7 +26551,50 @@ var init_intent_router = __esm({
|
|
|
26040
26551
|
/\bwhen\b.*\b(is\s+)?(inserted|updated|deleted|created)\b.*\b(trigger|fire)\b/,
|
|
26041
26552
|
/\bdo\b[^.?!]{0,120}\b(trigger|triggers)\b[^.?!]{0,80}\bfire\b/,
|
|
26042
26553
|
/\b(trigger|triggers)\b.*\b(both|and)\b.*\bfire\b/,
|
|
26043
|
-
/\b(same\s+transaction|rollup)\b.*\b(after[-\s]?insert|DLRS|dlrs)/i
|
|
26554
|
+
/\b(same\s+transaction|rollup)\b.*\b(after[-\s]?insert|DLRS|dlrs)/i,
|
|
26555
|
+
// FULL SAVE-ORDER / whole-transaction reconstruction (flow-family REACH).
|
|
26556
|
+
// "walk me through everything that fires in order", "list every/all
|
|
26557
|
+
// automation that fires when …", "full save order on X", "before-vs-after
|
|
26558
|
+
// -save breakdown of every automation" — the whole-order ask, distinct
|
|
26559
|
+
// from lifecycle-process (transition-value coupled) which owns the
|
|
26560
|
+
// "…becomes/is set to <value>" shapes above it.
|
|
26561
|
+
/\b(everything|every\s+automation|all\s+(?:the\s+)?automation)\b[^.?!]{0,60}\b(fires?|runs?|happens?)\b/,
|
|
26562
|
+
/\b(fires?|runs?|happens?)\b[^.?!]{0,40}\bin\s+order\b/,
|
|
26563
|
+
/\b(list|give\s+me)\b[^.?!]{0,40}\b(?:every|all)\b[^.?!]{0,20}\bautomation\b[^.?!]{0,50}\b(fires?|runs?|when)\b/,
|
|
26564
|
+
/\bfull\s+save\s+order\b/,
|
|
26565
|
+
/\bsave\s+order\b[^.?!]{0,40}\b(?:on|for)\b/,
|
|
26566
|
+
/\bbefore[-\s]?(?:vs[-\s]?)?after[-\s]?save\b[^.?!]{0,40}\b(breakdown|every\s+automation|automation)\b/,
|
|
26567
|
+
// "what order do validation rules and record-triggered flows evaluate" —
|
|
26568
|
+
// an explicit ordering question over multiple automation families.
|
|
26569
|
+
/\bwhat\s+order\b[^.?!]{0,90}\b(evaluate|run|fire|execute)\b/,
|
|
26570
|
+
// "run order between <FlowA> and <flow>" — the pairwise ordering ask.
|
|
26571
|
+
/\brun\s+order\s+between\b/,
|
|
26572
|
+
// "which apex classes are triggered when a X is inserted/created" — the
|
|
26573
|
+
// "apex classes" phrasing the "(apex|code|class)" pattern above missed
|
|
26574
|
+
// because it requires the singular "runs" verb, not "are triggered".
|
|
26575
|
+
/\b(?:which|what)\s+apex\s+classes?\b[^.?!]{0,40}\b(triggered|fire|run)\b[^.?!]{0,40}\b(insert|update|delete|save|creat)/,
|
|
26576
|
+
// "does anything run on X update that would collide with <Flow>" — an
|
|
26577
|
+
// impact-on-save question framed as "does anything run … on <dml>".
|
|
26578
|
+
/\bdoes\s+anything\s+(run|fire|happen)\b[^.?!]{0,40}\b(update|insert|save|edit|creat)/,
|
|
26579
|
+
// "what else fires on the same X save transaction" — co-firing on the same
|
|
26580
|
+
// transaction as a named automation.
|
|
26581
|
+
/\bwhat\s+else\s+(fires?|runs?|happens?)\b[^.?!]{0,60}\bsave\s+transaction\b/,
|
|
26582
|
+
// "assignment rules … run before or after the record-triggered flows" — the
|
|
26583
|
+
// classic order-of-execution question about where assignment rules sit.
|
|
26584
|
+
/\bassignment\s+rules?\b[^.?!]{0,60}\b(before|after)\b[^.?!]{0,40}\b(?:record[-\s]?triggered\s+)?flows?\b/,
|
|
26585
|
+
// "will it fight/collide/conflict with HEDA" when ADDING an after-save flow
|
|
26586
|
+
// — the build-planning ask whose answer is the existing save-order.
|
|
26587
|
+
/\b(?:after[-\s]?save|before[-\s]?save)\s+flow\b[^.?!]{0,80}\b(fight|collide|conflict|clash)\b/,
|
|
26588
|
+
// ORDER-OF-EXECUTION: "which TDTM/handler/trigger classes fire on X insert
|
|
26589
|
+
// and in what order" — the ordered trigger-handler question (HEDA TDTM
|
|
26590
|
+
// handlers register per DML event). "in what order" is the discriminator.
|
|
26591
|
+
/\b(?:which|what)\b[^.?!]{0,60}\b(?:tdtm|handler|trigger)\s+classes?\b[^.?!]{0,50}\bfire\b[^.?!]{0,50}\b(?:in\s+what\s+order|what\s+order|order)\b/,
|
|
26592
|
+
// "does <NamedFlow> run before or after <the> lead conversion" — a pairwise
|
|
26593
|
+
// ordering ask relative to a conversion; order_of_execution reconstructs it.
|
|
26594
|
+
// (lifecycle-process owns "what runs WHEN a Lead is converted"; this "run
|
|
26595
|
+
// before/after conversion" shape carries no such "what happens when" head,
|
|
26596
|
+
// so the two stay disjoint.)
|
|
26597
|
+
/\bruns?\s+(?:before|after)\b[^.?!]{0,60}\b(?:lead\s+conversion|converts?\s+the\s+lead|conversion)\b/
|
|
26044
26598
|
]
|
|
26045
26599
|
},
|
|
26046
26600
|
{
|
|
@@ -26174,7 +26728,42 @@ var init_intent_router = __esm({
|
|
|
26174
26728
|
/\bwhen\s+does\s+it\s+run\b/,
|
|
26175
26729
|
/\bdoes\b.*\bflow\b.*\b(system\s+mode|without\s+sharing|with\s+sharing)\b/,
|
|
26176
26730
|
/\b(system\s+mode|without\s+sharing|sharing\s+bypass)\b.*\bflow\b/,
|
|
26177
|
-
/\b(run|runs|running)\b.*\b(system\s+mode|without\s+sharing)\b.*\bflow\b
|
|
26731
|
+
/\b(run|runs|running)\b.*\b(system\s+mode|without\s+sharing)\b.*\bflow\b/,
|
|
26732
|
+
// NAMED-FLOW narration (flow-family REACH). A question that NAMES a
|
|
26733
|
+
// component by its API name (NAMED_COMPONENT_ID — a >=2-underscore token,
|
|
26734
|
+
// never English prose) AND asks to narrate/summarize/walk-through it is an
|
|
26735
|
+
// explain_flow ask. Each pattern REQUIRES the named id so a generic
|
|
26736
|
+
// "explain the sharing model" never lands here. resolve binds the entity
|
|
26737
|
+
// and the type-guard keeps explain_flow on a real Flow.
|
|
26738
|
+
// A COMPARE frame ("explain the difference between A and B", "compare …")
|
|
26739
|
+
// is NOT single-flow narration — explain_flow narrates ONE flow, so a
|
|
26740
|
+
// two-flow comparison must fall through (it stays a compare/unrouted gap
|
|
26741
|
+
// rather than a forced bad route). The `(?!…difference/compare/versus…)`
|
|
26742
|
+
// lookahead on the narration patterns below enforces that.
|
|
26743
|
+
new RegExp(`^(?!.*\\b(?:difference|differences|compare|comparison|versus|vs\\.?)\\b).*\\b(explain|summariz(?:e|ing)|walk\\s+me\\s+through|walkthrough|plain[-\\s]english\\s+walkthrough|purpose\\s+of)\\b[^.?!]{0,60}\\b${NAMED_COMPONENT_ID}\\b`),
|
|
26744
|
+
new RegExp(`\\b${NAMED_COMPONENT_ID}\\b[^.?!]{0,60}\\b(in\\s+plain\\s+(?:terms|english)|end\\s+to\\s+end|step\\s+by\\s+step)\\b`),
|
|
26745
|
+
// Narration verbs where the named id sits BEFORE the verb clause — e.g.
|
|
26746
|
+
// "Summarize <Name> and what it's calculating", "Explain <Name> and what
|
|
26747
|
+
// a 'flag' means", "What does <Name> copy from …". Same compare-frame
|
|
26748
|
+
// guard so "explain the difference between <A> and <B>" is not stolen.
|
|
26749
|
+
new RegExp(`^(?!.*\\b(?:difference|differences|compare|comparison|versus|vs\\.?)\\b).*\\b(?:explain|summarize|walk\\s+me\\s+through)\\b[^.?!]{0,20}\\b${NAMED_COMPONENT_ID}\\b`),
|
|
26750
|
+
new RegExp(`\\bwhat\\s+does\\b[^.?!]{0,20}\\b${NAMED_COMPONENT_ID}\\b[^.?!]{0,40}\\b(do|does|copy|copies|write|writes|calculat\\w*)\\b`),
|
|
26751
|
+
// "Does <Name> fire/run/write/have/send …" — a behavior question about a
|
|
26752
|
+
// NAMED flow (does it fire on insert+update, run every time, write to X,
|
|
26753
|
+
// have fault connectors, send texts). The named id as grammatical subject
|
|
26754
|
+
// of a flow-behavior verb keeps it precise.
|
|
26755
|
+
new RegExp(`\\bdoes\\s+(?:the\\s+)?${NAMED_COMPONENT_ID}\\b[^.?!]{0,80}\\b(fire|fires|run|runs|write|writes|have|has|send|sends|execute|re-?trigger)\\b`),
|
|
26756
|
+
// "Is <Name> a before-save/after-save/fast-field/scheduled/screen flow …" —
|
|
26757
|
+
// a flow-shape classification question about a named flow.
|
|
26758
|
+
new RegExp(`\\bis\\s+(?:the\\s+)?${NAMED_COMPONENT_ID}\\b[^.?!]{0,60}\\b(before[-\\s]?save|after[-\\s]?save|fast[-\\s]?field|scheduled\\s+path|screen\\s+flow|record[-\\s]?triggered)\\b`),
|
|
26759
|
+
// "What entry condition(s)/entry criteria gate/on <Name>" and "why would
|
|
26760
|
+
// <Name> skip a record" — entry-gate and skip-behavior narration.
|
|
26761
|
+
/\bentry\s+conditions?\b[^.?!]{0,20}\b(gate|on)\b/,
|
|
26762
|
+
/\bwhat(?:'s| is)?\s+the\s+entry\s+condition\b/,
|
|
26763
|
+
new RegExp(`\\bwhy\\s+would\\s+${NAMED_COMPONENT_ID}\\b[^.?!]{0,60}\\bskip\\b`),
|
|
26764
|
+
// "Does <Name> have any active version" / "why is it named like that" — the
|
|
26765
|
+
// active-version + naming question about a named flow.
|
|
26766
|
+
new RegExp(`\\bdoes\\s+${NAMED_COMPONENT_ID}\\b[^.?!]{0,40}\\b(active\\s+version|any\\s+active)\\b`)
|
|
26178
26767
|
]
|
|
26179
26768
|
},
|
|
26180
26769
|
{
|
|
@@ -26292,7 +26881,12 @@ var init_intent_router = __esm({
|
|
|
26292
26881
|
/\b(which|what)\s+tests?\b.*\b(run|chang|modif|deploy|impact|diff|edit)/,
|
|
26293
26882
|
/\btests?\b.*\bfor\s+(my|this|the|these)\s+(change|changes|diff|deploy|branch|pr|edit|edits)\b/,
|
|
26294
26883
|
/\b(test\s+impact|impacted\s+tests?|test\s+selection|minimal\s+(set\s+of\s+)?tests?|test\s+subset)\b/,
|
|
26295
|
-
/\bwhat\s+(do\s+i|should\s+i|to)\s+(run|test)\b.*\b(chang|deploy|diff)
|
|
26884
|
+
/\bwhat\s+(do\s+i|should\s+i|to)\s+(run|test)\b.*\b(chang|deploy|diff)/,
|
|
26885
|
+
// "which/what test class COVERS <X> (and does it test the bulk case)" —
|
|
26886
|
+
// which tests exercise a named class is the tests_for_change call-graph
|
|
26887
|
+
// walk; the trailing "bulk" qualifier must not drag it onto
|
|
26888
|
+
// governor_limit_risks (eval family C).
|
|
26889
|
+
/\b(?:which|what)\s+test\s+class(?:es)?\b[^.?!]{0,20}\bcovers?\b/
|
|
26296
26890
|
]
|
|
26297
26891
|
},
|
|
26298
26892
|
{
|
|
@@ -26313,8 +26907,19 @@ var init_intent_router = __esm({
|
|
|
26313
26907
|
/\b(fake|meaningless|empty|no\s+(real\s+)?)\s*assert/,
|
|
26314
26908
|
/\btests?\b.*\bno\s+(real\s+)?assert/,
|
|
26315
26909
|
/\bassertion\s+(quality|coverage)\b/,
|
|
26910
|
+
// "meaningful test audit on <X>" — the tool's own name as a natural-language
|
|
26911
|
+
// ask (USAGE/test-forensics REACH). Plus the classic no-op assertion tell
|
|
26912
|
+
// `System.assert(true)` / `assert(true)` that flags a rubber-stamp test.
|
|
26913
|
+
/\bmeaningful\s+test\s+audit\b/,
|
|
26914
|
+
/\bassert\w*\s*\(\s*true\s*\)/,
|
|
26316
26915
|
/\b(less\s+than|below|under)\b.*\b\d+\s*%\b.*\bcoverage\b/,
|
|
26317
26916
|
/\b(coverage|percent)\b.*\b(less\s+than|below|under)\b/,
|
|
26917
|
+
// "list apex classes below 75% coverage" — the `%\b` above never matches
|
|
26918
|
+
// "75% coverage" (%→space is not a word boundary), so the phrasing fell
|
|
26919
|
+
// through to the schema list rule and answered with get_component instead
|
|
26920
|
+
// of the coverage tools (eval family D).
|
|
26921
|
+
/\b(below|under|less\s+than)\s+\d+\s*(?:%|percent)?\s*(?:code\s+|test\s+)?coverage\b/,
|
|
26922
|
+
/\bcoverage\b[^.?!]{0,25}\b(below|under|less\s+than)\s+\d+/,
|
|
26318
26923
|
/\bwhich\s+apex\s+classes\b.*\bcoverage\b/,
|
|
26319
26924
|
/\bseealldata\s*=\s*true\b/i,
|
|
26320
26925
|
/\bsee\s+all\s+data\b/i,
|
|
@@ -26360,7 +26965,12 @@ var init_intent_router = __esm({
|
|
|
26360
26965
|
// "show performance risks in apex" — performance/scale risk phrasing the
|
|
26361
26966
|
// governor-limit recognizer answers. Battery gap.
|
|
26362
26967
|
/\bperformance\s+(risk|issue|problem|concern|bottleneck)s?\b/,
|
|
26363
|
-
|
|
26968
|
+
// "bulk" is a MODIFIER, not a head noun (eval family C): when the head
|
|
26969
|
+
// question is save-order ("what fires/runs/happens on X insert … what
|
|
26970
|
+
// runs bulk") or test coverage ("which test class covers Y … the bulk
|
|
26971
|
+
// case"), those earlier rules win by order — this guard keeps the bare
|
|
26972
|
+
// word from firing even when their patterns miss a phrasing.
|
|
26973
|
+
/^(?!.*\b(?:what\s+(?:fires|runs|happens)|test\s+class)\b).*\b(bulk|bulkif|unbounded)\b/,
|
|
26364
26974
|
/\bcpu\s+time\b/,
|
|
26365
26975
|
/\bheap\s+size\b/,
|
|
26366
26976
|
/\bmost\s+dml\b/,
|
|
@@ -26406,7 +27016,14 @@ var init_intent_router = __esm({
|
|
|
26406
27016
|
// extend the X package" — your customizations grafted onto a managed
|
|
26407
27017
|
// package's objects (the package_impact extensionCount surface).
|
|
26408
27018
|
/\b(what|which)\s+(of\s+(my|our)\s+)?(components?|metadata|customizations?|objects?|fields?)\b.*\bextends?\b/,
|
|
26409
|
-
/\bextends?\b.*\b(package|namespace|managed)\b
|
|
27019
|
+
/\bextends?\b.*\b(package|namespace|managed)\b/,
|
|
27020
|
+
// "what custom fields did <Package> INJECT across <objects>? Inventory for
|
|
27021
|
+
// UNINSTALL" — a managed-package boundary/uninstall inventory named by the
|
|
27022
|
+
// package rather than the literal word "package". The uninstall/inventory
|
|
27023
|
+
// frame + the inject/add-across verb keep it on package_impact and off the
|
|
27024
|
+
// generic field-usage tools (USAGE/IMPACT REACH — package boundary).
|
|
27025
|
+
/\b(inject\w*|add\w*|install\w*)\b[^.?!]{0,40}\bacross\b[^.?!]{0,60}\b(objects?|lead|contact|account|case|opportunity)\b[^]*\b(uninstall|inventory)\b/,
|
|
27026
|
+
/\binventory\s+for\s+uninstall\b/
|
|
26410
27027
|
]
|
|
26411
27028
|
},
|
|
26412
27029
|
{
|
|
@@ -26535,7 +27152,16 @@ var init_intent_router = __esm({
|
|
|
26535
27152
|
/\brelease\s+readiness\b/,
|
|
26536
27153
|
/\b(ready|readiness)\b.*\b(release|deploy|go[-\s]?live|cutover|production)\b/,
|
|
26537
27154
|
/\bpre[-\s]?release\b.*\b(check|review|audit)\b/,
|
|
26538
|
-
/\bgo[-\s]?live\b.*\b(risk|readiness|checklist)\b
|
|
27155
|
+
/\bgo[-\s]?live\b.*\b(risk|readiness|checklist)\b/,
|
|
27156
|
+
// DISCOVERY/META REACH: "is this org release-ready for the summer push,
|
|
27157
|
+
// or are there blockers" — the hyphenated "release-ready" adjective and
|
|
27158
|
+
// the "blockers" framing the patterns above missed (they keyed on
|
|
27159
|
+
// "release readiness"/"ready … [to] release"). Co-anchored on
|
|
27160
|
+
// release/deploy/ship so a generic "are we ready for the demo" (no
|
|
27161
|
+
// release verb) does not match.
|
|
27162
|
+
/\brelease[-\s]?ready\b/,
|
|
27163
|
+
/\b(?:blockers?|showstoppers?)\b[^.?!]{0,40}\b(?:release|deploy|ship|go[-\s]?live|production|cutover)\b/,
|
|
27164
|
+
/\b(?:release|deploy|ship|go[-\s]?live|production|cutover)\b[^.?!]{0,40}\b(?:blockers?|showstoppers?)\b/
|
|
26539
27165
|
]
|
|
26540
27166
|
},
|
|
26541
27167
|
// === Vault health / freshness / coverage (vault) ==========================
|
|
@@ -26628,6 +27254,27 @@ var init_intent_router = __esm({
|
|
|
26628
27254
|
/\bmetadata\b.*\b(documented|documentation)\b/
|
|
26629
27255
|
]
|
|
26630
27256
|
},
|
|
27257
|
+
{
|
|
27258
|
+
// API-version hygiene ("any apex still on API version below 50?") was
|
|
27259
|
+
// unrouted — the schema rule only covers the single-component "what is the
|
|
27260
|
+
// api version of X" lookup (eval family D). tech_debt_score carries the
|
|
27261
|
+
// roll-up (apexBelowApiVersion30/40/50Count) and list_components(type:
|
|
27262
|
+
// ApexClass) enumerates the classes with each node's apiVersion.
|
|
27263
|
+
intent: "api-version-audit",
|
|
27264
|
+
plane: "vault",
|
|
27265
|
+
tools: ["sfi.tech_debt_score", "sfi.list_components"],
|
|
27266
|
+
liveRequired: false,
|
|
27267
|
+
needsResolve: false,
|
|
27268
|
+
reason: "Old-API-version inventory: tech_debt_score counts Apex below API version 30/40/50, and list_components(type: ApexClass) carries each class apiVersion.",
|
|
27269
|
+
suggestArgs: () => ({ type: "ApexClass" }),
|
|
27270
|
+
patterns: [
|
|
27271
|
+
/\bapi\s+versions?\b[^.?!]{0,25}\b(below|under|older|before|less\s+than)\b/,
|
|
27272
|
+
/\b(old|outdated|legacy|ancient)\b[^.?!]{0,15}\bapi\s+versions?\b/,
|
|
27273
|
+
/\b(apex|class(es)?|components?|metadata|flows?|triggers?)\b[^.?!]{0,40}\b(still\s+on|stuck\s+on|running\s+on)\b[^.?!]{0,15}\bapi\s+version/,
|
|
27274
|
+
/\b(upgrad|updat|bump)\w*\b[^.?!]{0,25}\bapi\s+versions?\b/,
|
|
27275
|
+
/\bapi\s+versions?\b[^.?!]{0,25}\b(upgrad|updat|bump)/
|
|
27276
|
+
]
|
|
27277
|
+
},
|
|
26631
27278
|
{
|
|
26632
27279
|
// "Which classes implement <interface>" (Batchable, Schedulable, Queueable,
|
|
26633
27280
|
// RestResource, ...) — grep Apex source for the implements clause. The
|
|
@@ -26657,7 +27304,32 @@ var init_intent_router = __esm({
|
|
|
26657
27304
|
patterns: [
|
|
26658
27305
|
/\bfind\b.*\b(class|apex|code)\b.*\b(mentions?|references?|uses?|calls?|reads?|writes?|with)\b/,
|
|
26659
27306
|
/\b(which|what)\s+(classes?|apex)\b.*\b(mentions?|references?|uses?|touch(es)?|reads?|writes?|calls?|invokes?)\b/,
|
|
26660
|
-
/\bsearch\b.*\b(apex|code)\b
|
|
27307
|
+
/\bsearch\b.*\b(apex|code)\b/,
|
|
27308
|
+
// "which classes make HTTP callouts and what endpoints do they hit" —
|
|
27309
|
+
// HTTP callouts live in Apex SOURCE, so the answer is a source grep,
|
|
27310
|
+
// not the outbound endpoint catalog and never field lineage (eval
|
|
27311
|
+
// family C). "What endpoints do we call out to?" (no "callout" noun,
|
|
27312
|
+
// no HTTP) stays on the outbound endpoints catalog below.
|
|
27313
|
+
/\bhttp\s+callouts?\b/,
|
|
27314
|
+
/\bcallouts?\b[^.?!]{0,50}\bendpoints?\b/
|
|
27315
|
+
]
|
|
27316
|
+
},
|
|
27317
|
+
{
|
|
27318
|
+
// "Does <the named class/job> run async — in its own transaction?" is a
|
|
27319
|
+
// question about ONE component's actual implementation (its @future /
|
|
27320
|
+
// Queueable / Batchable shape, read from its source via get_component),
|
|
27321
|
+
// not a best-practice lecture — the "async" qualifier was dragging it
|
|
27322
|
+
// onto the generic knowledge-plane guidance rule (eval family C).
|
|
27323
|
+
intent: "async-transaction-context",
|
|
27324
|
+
plane: "vault",
|
|
27325
|
+
tools: ["sfi.resolve", "sfi.get_component"],
|
|
27326
|
+
liveRequired: false,
|
|
27327
|
+
needsResolve: true,
|
|
27328
|
+
reason: "Whether a named component runs async in its own transaction is read from its OWN source/metadata (resolve it, then get_component shows the @future/Queueable/Batchable shape) \u2014 not generic async guidance.",
|
|
27329
|
+
patterns: [
|
|
27330
|
+
/\basync\w*\b[^.?!]{0,80}\bown\s+transaction\b/,
|
|
27331
|
+
/\bown\s+transaction\b[^.?!]{0,80}\basync\w*/,
|
|
27332
|
+
/\bin\s+its\s+own\s+transaction\b/
|
|
26661
27333
|
]
|
|
26662
27334
|
},
|
|
26663
27335
|
// === Integration (vault) ==================================================
|
|
@@ -26689,9 +27361,15 @@ var init_intent_router = __esm({
|
|
|
26689
27361
|
liveRequired: false,
|
|
26690
27362
|
needsResolve: false,
|
|
26691
27363
|
reason: "Topology of the org's integration surfaces (named creds, connected apps, remote sites, external services).",
|
|
27364
|
+
// Guarded (eval family C — qualifier hijack): "integration"/vendor-sync
|
|
27365
|
+
// words are MODIFIERS when the head question is a what-if simulation
|
|
27366
|
+
// ("if the field type changed, what integrations blow up"), a blast
|
|
27367
|
+
// radius ("blast radius if I disable trigger T"), a field-write audit,
|
|
27368
|
+
// or profile IP security — those heads route earlier / later on their
|
|
27369
|
+
// own rules, so the bare noun must not force the topology catalog.
|
|
26692
27370
|
patterns: [
|
|
26693
|
-
|
|
26694
|
-
|
|
27371
|
+
/^(?!.*\b(?:what\s+if|blast\s+radius|what\s+breaks|blows?\s+up|field\s+type|data\s+type|written\s+(?:only\s+)?by|only\s+ever\s+written|ip\s+relax\w*|disabl\w+)\b).*\b(integrations?|named\s+credentials?|connected\s+apps?|remote\s+sites?|external\s+services?|auth\s+providers?)\b/,
|
|
27372
|
+
/^(?!.*\b(?:what\s+if|blast\s+radius|what\s+breaks|blows?\s+up|field\s+type|data\s+type|written\s+(?:only\s+)?by|only\s+ever\s+written|ip\s+relax\w*|disabl\w+)\b).*\bwhat\b.*\bintegrat/,
|
|
26695
27373
|
/\bapi\b.*\b(connections?|surfaces?)\b/
|
|
26696
27374
|
]
|
|
26697
27375
|
},
|
|
@@ -26706,7 +27384,17 @@ var init_intent_router = __esm({
|
|
|
26706
27384
|
/\b(platform\s+events?|change\s+data\s+capture|cdc)\b.*\b(subscrib|listen|consum)\b/,
|
|
26707
27385
|
/\bwho\s+(subscribes?|listens?)\b/,
|
|
26708
27386
|
/\b(subscribers?|subscriptions?)\b.*\bevents?\b/,
|
|
26709
|
-
/\bshow\s+event\s+subscribers?\b
|
|
27387
|
+
/\bshow\s+event\s+subscribers?\b/,
|
|
27388
|
+
// CDC fan-out / subscriber discovery (flow-family REACH). "what's
|
|
27389
|
+
// SUBSCRIBING TO Change Data Capture" (verb precedes the noun, so the
|
|
27390
|
+
// noun→verb pattern above misses it), "if turning on CDC for X will fan
|
|
27391
|
+
// out", and "CDC or platform-event subscribers feeding Marketo" (hyphenated
|
|
27392
|
+
// "platform-event" the space-only alternation above misses). Anchored to a
|
|
27393
|
+
// CDC/platform-event noun so a generic "who subscribes" is unaffected.
|
|
27394
|
+
/\b(subscrib\w*)\b[^.?!]{0,30}\b(change\s+data\s+capture|cdc|platform[-\s]?events?)\b/,
|
|
27395
|
+
/\b(turn\w*\s+on|enabl\w+)\s+cdc\b/,
|
|
27396
|
+
/\bcdc\b[^.?!]{0,40}\bfan\s+out\b/,
|
|
27397
|
+
/\b(cdc|change\s+data\s+capture|platform[-\s]?events?)\b[^.?!]{0,40}\bsubscribers?\b/
|
|
26710
27398
|
]
|
|
26711
27399
|
},
|
|
26712
27400
|
{
|
|
@@ -26867,7 +27555,15 @@ var init_intent_router = __esm({
|
|
|
26867
27555
|
/\bdeactivating\b.*\b(break|would)\b/,
|
|
26868
27556
|
/\breplacing\b.*\bwith\b.*\b(lwc|aura)\b/,
|
|
26869
27557
|
/\baffected\b.*\b(removing|remove)\b.*\brecord\s+type/,
|
|
26870
|
-
/\bwhat\s+would\s+break\b.*\b(changed|change)\b
|
|
27558
|
+
/\bwhat\s+would\s+break\b.*\b(changed|change)\b/,
|
|
27559
|
+
// "what if I REMOVED the <X> record type … which page layouts and flows
|
|
27560
|
+
// assume it exists" — a record-type deletion blast radius (IMPACT REACH).
|
|
27561
|
+
// `\bremove\b` misses the PAST tense "removed", so this fell through to
|
|
27562
|
+
// unrouted. High-precision: the remove/delete verb must co-occur with the
|
|
27563
|
+
// literal "record type" AND a what-if/assume/kept frame, so it never steals
|
|
27564
|
+
// a field-type what-if (those say "field"/"required"/"picklist value").
|
|
27565
|
+
/\b(remov\w*|delet\w*|drop\w*)\b[^.?!]{0,40}\brecord\s+type\b[^]*\b(assume|assumes?|kept|keep|rely|relies|reference|expect)\w*/,
|
|
27566
|
+
/\bwhat\s+if\s+i\s+(remov\w*|delet\w*|drop\w*)\b[^.?!]{0,40}\brecord\s+type\b/
|
|
26871
27567
|
]
|
|
26872
27568
|
},
|
|
26873
27569
|
{
|
|
@@ -26886,7 +27582,32 @@ var init_intent_router = __esm({
|
|
|
26886
27582
|
/\b(change|convert)\b.*\bfield\s+type\b/,
|
|
26887
27583
|
/\b(change|changed)\b.*\bdata\s+type\b/,
|
|
26888
27584
|
/\bdata\s+type\b.*\bchange/,
|
|
26889
|
-
|
|
27585
|
+
// PASSIVE / noun-first form — "if the field type CHANGED, what
|
|
27586
|
+
// integrations blow up": \bchange\b never matches "changed", so the
|
|
27587
|
+
// phrasing fell through and the "integrations" qualifier hijacked it
|
|
27588
|
+
// onto integration_map (eval family C).
|
|
27589
|
+
/\bfield\s+type\b[^.?!]{0,60}\bchang/,
|
|
27590
|
+
/\bremove\b.*\bpicklist\s+value\b/,
|
|
27591
|
+
// FIELD-FORENSICS REACH (what_if_make_field_required / _change_field_type).
|
|
27592
|
+
// The `\bmake\b` and `\bchange\b` verb anchors above miss the PAST tense
|
|
27593
|
+
// ("if we MADE X required", "if I CHANGED X") and the noun form
|
|
27594
|
+
// ("required-field validation"), so these fell to unrouted. Each new
|
|
27595
|
+
// pattern REQUIRES a required/field-type/picklist frame near the verb, so
|
|
27596
|
+
// it stays a schema what-if and never steals a value-change or usage ask.
|
|
27597
|
+
// "if we made <field> required" / "made X required but left Y optional".
|
|
27598
|
+
/\bmade\b[^.?!]{0,40}\brequired\b/,
|
|
27599
|
+
// "adding a required-field validation on <Field>" — making a field
|
|
27600
|
+
// mandatory expressed as a validation-rule ask; the required frame keeps
|
|
27601
|
+
// it on what_if_make_field_required.
|
|
27602
|
+
/\brequired[-\s]field\b/,
|
|
27603
|
+
/\b(add\w*|making|makes)\b[^.?!]{0,30}\brequired\b/,
|
|
27604
|
+
// "what happens if I change <field> FROM a picklist TO a text field" — a
|
|
27605
|
+
// field-TYPE conversion named by the from/to types (picklist→text) rather
|
|
27606
|
+
// than the literal "field type". The from-X-to-Y frame around a field-type
|
|
27607
|
+
// noun (picklist/text/number/checkbox/formula/lookup/date/currency) is
|
|
27608
|
+
// unambiguously a type change.
|
|
27609
|
+
new RegExp(`\\b(chang\\w*|convert\\w*)\\b[^.?!]{0,60}\\bfrom\\b[^.?!]{0,40}\\bto\\b[^.?!]{0,40}\\b(picklist|text|number|checkbox|formula|lookup|date|currency|multi[-\\s]?select)\\b`),
|
|
27610
|
+
new RegExp(`\\bfrom\\s+a\\s+picklist\\s+to\\s+a\\s+(?:text|number|formula|lookup|date|currency)\\b`)
|
|
26890
27611
|
]
|
|
26891
27612
|
},
|
|
26892
27613
|
{
|
|
@@ -27102,11 +27823,22 @@ var init_intent_router = __esm({
|
|
|
27102
27823
|
tools: ["sfi.compare_vaults", "sfi.compare_object_across_vaults", "sfi.compare_profile_across_vaults"],
|
|
27103
27824
|
liveRequired: false,
|
|
27104
27825
|
needsResolve: false,
|
|
27105
|
-
reason: "Differences between two registered orgs (UAT vs prod) from the vault registry.",
|
|
27826
|
+
reason: "Differences between two registered orgs (UAT vs prod) from the vault registry. Requires BOTH orgs to be registered vaults \u2014 the compare_* tools diff two offline snapshots, never the live org.",
|
|
27827
|
+
// DISCLOSURE, not a block (eval family D): on a single-vault install the
|
|
27828
|
+
// compare tools fail with vault-not-found — say the second-registered-vault
|
|
27829
|
+
// prerequisite up front instead of routing confident-clean into it.
|
|
27830
|
+
gap: {
|
|
27831
|
+
category: "cross-vault-registry",
|
|
27832
|
+
note: "Cross-vault comparison needs a SECOND registered vault (a multi-vault registry). If only this vault is registered, the compare_* call will return vault-not-found \u2014 register the other org first (sfi vault register) or name the two vault aliases to compare."
|
|
27833
|
+
},
|
|
27106
27834
|
patterns: [
|
|
27107
27835
|
/\b(uat|sandbox|staging)\b.*\b(vs|versus|compared?\s+to|and)\b.*\b(prod|production)\b/,
|
|
27108
27836
|
/\bwhat('?s| is)\s+different\b.*\b(between|across)\b.*\borgs?\b/,
|
|
27109
|
-
/\bcompare\b.*\borgs?\b
|
|
27837
|
+
/\bcompare\b.*\borgs?\b/,
|
|
27838
|
+
// "compare the Account object across our sandboxes" — the cross-vault
|
|
27839
|
+
// ask phrased with "across" + an environment noun (eval family D).
|
|
27840
|
+
/\bcompare\b[^.?!]{0,60}\bacross\b[^.?!]{0,30}\b(orgs?|sandbox(es)?|environments?|vaults?|instances?)\b/,
|
|
27841
|
+
/\b(differs?|difference|different)\b[^.?!]{0,50}\b(between|across)\b[^.?!]{0,40}\b(sandbox(es)?|environments?|instances?|vaults?)\b/
|
|
27110
27842
|
]
|
|
27111
27843
|
},
|
|
27112
27844
|
{
|
|
@@ -27229,7 +27961,115 @@ var init_intent_router = __esm({
|
|
|
27229
27961
|
/\b(ci\/cd|source\s+control|deployment\s+(and\s+)?release)\b/,
|
|
27230
27962
|
/\bsandboxes?\b.*\b(refresh|managed)\b/,
|
|
27231
27963
|
/\benvironment\s+and\s+release\s+strategy\b/,
|
|
27232
|
-
/\b(is\s+there\s+a\s+)?ci\/cd\b
|
|
27964
|
+
/\b(is\s+there\s+a\s+)?ci\/cd\b/,
|
|
27965
|
+
// Concept ask about an access primitive — "What is a Profile" / "what is
|
|
27966
|
+
// a permission set". The indefinite article marks a GENERIC type word,
|
|
27967
|
+
// never a named component, so this must not fall through to unrouted (or
|
|
27968
|
+
// worse, a Profile-record disambiguation menu). Comparisons ("difference
|
|
27969
|
+
// between a profile and a permission set") stay on compare-profiles.
|
|
27970
|
+
/\bwhat\s+is\s+an?\s+(?:profile|permission\s+set)\s*\??\s*$/
|
|
27971
|
+
]
|
|
27972
|
+
},
|
|
27973
|
+
// === FIELD-FORENSICS REACH block (USAGE / IMPACT / FIELD-FORENSICS cluster) =
|
|
27974
|
+
// These sit BEFORE the generic component-usage dispatcher so a question that
|
|
27975
|
+
// NAMES a specific field (NAMED_FIELD_ID — a dotted `Object.field` or a bare
|
|
27976
|
+
// `__c` api name) and asks a field-forensics question lands on the dedicated
|
|
27977
|
+
// field tool instead of the generic usage tool. Every pattern REQUIRES the
|
|
27978
|
+
// named field, so a bare-English question (no dot, no `__c`) never fires here
|
|
27979
|
+
// and keeps its existing route. The order within the block encodes the eval's
|
|
27980
|
+
// distinction: "reads OR writes" → find_field_anywhere; "what writes … is it a
|
|
27981
|
+
// flow" (writers only) → field_provenance; a lineage/trace frame →
|
|
27982
|
+
// field_lineage; "field 360" → field_360; a semantic "do we have a field for
|
|
27983
|
+
// X" → find_semantic_field. USAGE ("which flows write X", "is X triggered by
|
|
27984
|
+
// Y", "connected to Marketo") stays on the component-usage dispatcher below.
|
|
27985
|
+
{
|
|
27986
|
+
// "do we already have a field for <concept>" / "where do we store <concept>
|
|
27987
|
+
// data across the org" — semantic field discovery. High-precision on the
|
|
27988
|
+
// store/have-a-field frame + a cross-org scope; must sit before the field-id
|
|
27989
|
+
// rules because it is about a CONCEPT, not a specific named field (the named
|
|
27990
|
+
// field it cites, e.g. "X is one, what ELSE", is an example, not the target).
|
|
27991
|
+
intent: "find-semantic-field",
|
|
27992
|
+
plane: "vault",
|
|
27993
|
+
tools: ["sfi.find_semantic_field"],
|
|
27994
|
+
liveRequired: false,
|
|
27995
|
+
needsResolve: false,
|
|
27996
|
+
reason: 'Semantic field discovery \u2014 "do we already have a field for X" ranks CustomFields by token overlap with a natural-language concept (heuristic recommendation).',
|
|
27997
|
+
patterns: [
|
|
27998
|
+
/\bwhere\s+do\s+we\s+store\b[^.?!]{0,60}\b(across|anywhere|in\s+(?:the\s+)?org)\b/,
|
|
27999
|
+
/\bdo\s+we\s+(?:already\s+)?have\s+a\s+field\s+for\b/,
|
|
28000
|
+
/\b(is|are)\s+there\s+(?:already\s+)?an?\s+(?:existing\s+)?field\s+for\b/,
|
|
28001
|
+
// "X is one, what else" — the caller names one example field and asks for
|
|
28002
|
+
// the rest of the concept family (the semantic-discovery signature).
|
|
28003
|
+
/\bis\s+one,?\s+what\s+else\b/
|
|
28004
|
+
]
|
|
28005
|
+
},
|
|
28006
|
+
{
|
|
28007
|
+
// "trace where <field> goes after conversion / where does <field> data come
|
|
28008
|
+
// from / where does <field> flow" — data lineage for a NAMED field. Sits
|
|
28009
|
+
// before find_field_anywhere/provenance so an explicit trace/lineage frame
|
|
28010
|
+
// wins over a bare reads/writes ask. The named-field id + a movement verb
|
|
28011
|
+
// (trace/goes/lands/flows/come from) keep it precise; the pii-flow rule
|
|
28012
|
+
// earlier already owns the "field data flow/lineage" phrasings without a
|
|
28013
|
+
// named id, so this only adds the NAMED-field trace shape it missed.
|
|
28014
|
+
intent: "field-lineage",
|
|
28015
|
+
plane: "vault",
|
|
28016
|
+
tools: ["sfi.resolve", "sfi.field_lineage"],
|
|
28017
|
+
liveRequired: false,
|
|
28018
|
+
needsResolve: true,
|
|
28019
|
+
reason: "Trace where a NAMED field's value comes from and what it feeds \u2014 the provenance + downstream-effects walker, requested by an explicit trace/lineage frame on a named field.",
|
|
28020
|
+
patterns: [
|
|
28021
|
+
new RegExp(`\\btrace\\b[^.?!]{0,40}\\b${NAMED_FIELD_ID}\\b[^.?!]{0,60}\\b(goes?|lands?|flows?|end\\s+up|get\\s+dropped|come\\s+from|after\\s+conversion)\\b`),
|
|
28022
|
+
new RegExp(`\\bwhere\\s+does\\b[^.?!]{0,30}\\b${NAMED_FIELD_ID}\\b[^.?!]{0,50}\\b(go|goes?|flow|flows?|come\\s+from|land)\\b`)
|
|
28023
|
+
]
|
|
28024
|
+
},
|
|
28025
|
+
{
|
|
28026
|
+
// "give me the field 360 on <Field>" / "the full picture of <Field>" — the
|
|
28027
|
+
// unified field-forensics synthesis tool, requested by its own vocabulary.
|
|
28028
|
+
// The literal "field 360" / "360 on <field>" phrase is unambiguous.
|
|
28029
|
+
intent: "field-360",
|
|
28030
|
+
plane: "vault",
|
|
28031
|
+
tools: ["sfi.resolve", "sfi.field_360", "sfi.explain_field"],
|
|
28032
|
+
liveRequired: false,
|
|
28033
|
+
needsResolve: true,
|
|
28034
|
+
reason: "Full 360 profile of a single field \u2014 everything that validates / writes / reads / uses it across automation, code, UI, integrations, composed into one report.",
|
|
28035
|
+
patterns: [
|
|
28036
|
+
/\bfield\s*360\b/,
|
|
28037
|
+
/\b360\b[^.?!]{0,20}\b(on|of|for)\b[^.?!]{0,20}(?:[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*|[a-z][a-z0-9_]*__c)\b/,
|
|
28038
|
+
new RegExp(`\\b(fulls+(?:picture|profile)|everythings+thats+(?:touches|uses))\\b[^.?!]{0,30}\\b${NAMED_FIELD_ID}\\b`)
|
|
28039
|
+
]
|
|
28040
|
+
},
|
|
28041
|
+
{
|
|
28042
|
+
// "What READS OR WRITES <Field> on <Object>" — the universal find-anywhere
|
|
28043
|
+
// for a named field (both directions). The "reads or/and writes" (or the
|
|
28044
|
+
// "used anywhere") frame distinguishes it from provenance (writers-only)
|
|
28045
|
+
// below. Requires the named field id so bare English never fires.
|
|
28046
|
+
intent: "find-field-anywhere-usage",
|
|
28047
|
+
plane: "vault",
|
|
28048
|
+
tools: ["sfi.resolve", "sfi.find_field_anywhere"],
|
|
28049
|
+
liveRequired: false,
|
|
28050
|
+
needsResolve: true,
|
|
28051
|
+
reason: "Where a NAMED field is used anywhere \u2014 every incoming edge (apex reads/writes, flow ops, layout placements, VR refs), grouped by referrer type. Both read and write directions.",
|
|
28052
|
+
patterns: [
|
|
28053
|
+
new RegExp(`\\b(reads?\\s+(?:or|and)\\s+writes?|writes?\\s+(?:or|and)\\s+reads?)\\b[^.?!]{0,60}\\b${NAMED_FIELD_ID}\\b`),
|
|
28054
|
+
new RegExp(`\\b${NAMED_FIELD_ID}\\b[^.?!]{0,40}\\bused\\s+anywhere\\b`)
|
|
28055
|
+
]
|
|
28056
|
+
},
|
|
28057
|
+
{
|
|
28058
|
+
// "What WRITES <Field> on <Object> — is it a flow?" — the source-of-a-field
|
|
28059
|
+
// ask (writers only): who/what SETS this field's value, and is it manual /
|
|
28060
|
+
// automated / integration-synced. Distinguished from find-field-anywhere
|
|
28061
|
+
// above by being writers-only (no "reads"), and from the "which flows write
|
|
28062
|
+
// X" USAGE ask below by NOT scoping to a component type up front (the field
|
|
28063
|
+
// is the subject: "what writes X"). Requires the named field id.
|
|
28064
|
+
intent: "field-provenance",
|
|
28065
|
+
plane: "vault",
|
|
28066
|
+
tools: ["sfi.resolve", "sfi.field_provenance"],
|
|
28067
|
+
liveRequired: false,
|
|
28068
|
+
needsResolve: true,
|
|
28069
|
+
reason: "Who/what SETS a field's value \u2014 the writers fabric (apex/flow/trigger writers, formula/auto-number declaration, integration-synced classifier) for a named field.",
|
|
28070
|
+
patterns: [
|
|
28071
|
+
new RegExp(`^(?!.*\\breads?\\b)(?!.*\\bwhich\\s+flows?\\b).*\\bwhat\\s+writes\\b[^.?!]{0,80}\\b${NAMED_FIELD_ID}\\b`),
|
|
28072
|
+
new RegExp(`\\bwhat\\s+(?:sets|populates?|fills?)\\b[^.?!]{0,60}\\b${NAMED_FIELD_ID}\\b[^.?!]{0,40}\\bis\\s+it\\s+(?:a\\s+)?(?:flow|manual|automated|integration)`)
|
|
27233
28073
|
]
|
|
27234
28074
|
},
|
|
27235
28075
|
{
|
|
@@ -27260,7 +28100,33 @@ var init_intent_router = __esm({
|
|
|
27260
28100
|
// usage lands here.
|
|
27261
28101
|
/\bwhere\s+is\b(?![^?]*\bfield\b)(?![^?]*__c)[^?]*\b(used|referenced|consumed)\b/,
|
|
27262
28102
|
/\b(what|who|which\s+components?)\b.*\b(uses?|references?|depends?\s+on|consumes?)\b/,
|
|
27263
|
-
/\bwhat\s+still\s+references\b
|
|
28103
|
+
/\bwhat\s+still\s+references\b/,
|
|
28104
|
+
// USAGE REACH (find_component_usages). "Which flows WRITE (to) <field>" —
|
|
28105
|
+
// the writers of a field scoped to a component TYPE (flows). Distinct from
|
|
28106
|
+
// field_provenance ("what writes X") by naming the referrer type first;
|
|
28107
|
+
// find_component_usages is the right dispatcher for "which <type> touch X".
|
|
28108
|
+
/\bwhich\s+(?:flows?|classes?|triggers?|automations?|processes?)\b[^.?!]{0,20}\b(writes?|write\s+to|updates?|reads?|references?|touch\w*|sets?)\b/,
|
|
28109
|
+
// "which flow SHOULD (do X / update Y) and why isn't it running" — a
|
|
28110
|
+
// which-component-does-this usage lookup phrased as a troubleshooting ask.
|
|
28111
|
+
/\bwhich\s+(?:flow|class|automation|process)\b[^.?!]{0,30}\bshould\b/,
|
|
28112
|
+
// "is <NamedFlow> triggered by <OtherFlow> or standalone" — whether one
|
|
28113
|
+
// component invokes another (an incoming-edge usage question).
|
|
28114
|
+
/\bis\s+(?:the\s+)?[a-z][a-z0-9_]*_[a-z0-9_]+\b[^.?!]{0,30}\btriggered\s+by\b/,
|
|
28115
|
+
// "does updating <field> trigger any <X> automation" — whether writing a
|
|
28116
|
+
// field fires downstream automation (the field's incoming/outgoing usage).
|
|
28117
|
+
// `[^?!]` (not `[^.?!]`) so the gap can span the dotted `Object.field__c`
|
|
28118
|
+
// name, whose `.` the standard class would otherwise stop at.
|
|
28119
|
+
/\bdoes\s+updating\b[^?!]{0,50}\btrigger\b[^?!]{0,30}\bautomation\b/,
|
|
28120
|
+
// "is <object> connected to <ExternalSystem>" — integration-usage of a
|
|
28121
|
+
// component; find_component_usages composes graph edges + the integration
|
|
28122
|
+
// map (the rule already carries integration_map as a secondary tool).
|
|
28123
|
+
/\b(is|are)\b[^.?!]{0,40}\b(connected\s+to|integrated\s+with|synced?\s+(?:to|with)|feeding)\b[^.?!]{0,30}\b(marketo|pardot|hubspot|external|api)\b/,
|
|
28124
|
+
// "is <NamedComponent> even/still needed anymore BASED ON USAGE" — a
|
|
28125
|
+
// still-in-use check on a named component, answered by walking its usage
|
|
28126
|
+
// edges. The "based on usage" / "still used anywhere" frame is the tell;
|
|
28127
|
+
// it keeps this off the cleanup-catalog tools (which take no named id).
|
|
28128
|
+
/\b(?:even|still)\s+(?:needed|used|referenced)\b[^?!]{0,20}\b(?:anymore\s+)?based\s+on\s+usage\b/,
|
|
28129
|
+
/\bstill\s+(?:used|referenced)\s+anywhere\b/
|
|
27264
28130
|
]
|
|
27265
28131
|
},
|
|
27266
28132
|
{
|
|
@@ -27476,6 +28342,121 @@ var init_intent_router = __esm({
|
|
|
27476
28342
|
/\bcompare\b.*\b(permission\s+sets?|profiles?)\b/
|
|
27477
28343
|
]
|
|
27478
28344
|
},
|
|
28345
|
+
{
|
|
28346
|
+
// DISCOVERY/META REACH: "compare A and B" / "explain the difference between
|
|
28347
|
+
// A and B" where A and B are TWO NAMED components (permission-set groups,
|
|
28348
|
+
// flows, classes) — a two-component diff, which compare_components answers.
|
|
28349
|
+
// Sits AFTER compare-profiles (that owns the "profiles"/"permission sets"
|
|
28350
|
+
// wording) and AFTER explain-flow (whose narration patterns carry a
|
|
28351
|
+
// compare-frame negative-lookahead, so a two-flow "difference between A and
|
|
28352
|
+
// B" deliberately falls through to HERE rather than being narrated as one
|
|
28353
|
+
// flow). Precision: every pattern requires TWO distinct named tokens joined
|
|
28354
|
+
// by "and"/"vs"/"versus" (or the explicit plural "PSGs"/"perm groups"), so
|
|
28355
|
+
// a single-component "explain X" never lands here.
|
|
28356
|
+
intent: "compare-components",
|
|
28357
|
+
plane: "vault",
|
|
28358
|
+
tools: ["sfi.resolve", "sfi.compare_components"],
|
|
28359
|
+
liveRequired: false,
|
|
28360
|
+
needsResolve: true,
|
|
28361
|
+
reason: "Comparing two named components is a vault diff \u2014 resolve both, then compare_components.",
|
|
28362
|
+
patterns: [
|
|
28363
|
+
// "compare the <NamedA> and <NamedB> PSGs" / "… perm groups" — an
|
|
28364
|
+
// explicit two-thing compare of permission-set GROUPS (compare-profiles
|
|
28365
|
+
// keys on "profiles"/"permission sets", not the "PSG"/"perm group" plural).
|
|
28366
|
+
new RegExp(`\\bcompare\\b[^.?!]{0,80}\\b(?:psgs?|perm(?:ission)?\\s+set\\s+groups?|perm\\s+groups?)\\b`),
|
|
28367
|
+
new RegExp(`\\b(?:psgs?|perm(?:ission)?\\s+set\\s+groups?|perm\\s+groups?)\\b[^.?!]{0,80}\\b(?:compare|difference|differ|versus|vs\\.?)\\b`),
|
|
28368
|
+
// "compare/difference between <NamedA> and <NamedB>" — two API-name tokens
|
|
28369
|
+
// (>=2 underscores each: unambiguously component ids, never prose) joined
|
|
28370
|
+
// by "and"/"&". The two-id requirement is what distinguishes a real
|
|
28371
|
+
// comparison from a single-component narration. The leading
|
|
28372
|
+
// `(?!.*record\s+types?)` yields a "difference between A and B and C record
|
|
28373
|
+
// TYPES" enumeration to record-type-enumeration below (list all types),
|
|
28374
|
+
// which is the honest surface for several record types side by side.
|
|
28375
|
+
new RegExp(`^(?!.*\\brecord\\s+types?\\b).*\\b(?:compare|difference|differ|versus|vs\\.?)\\b[^.?!]{0,40}\\b${NAMED_COMPONENT_ID}\\b[^.?!]{0,20}\\b(?:and|&|versus|vs\\.?)\\b[^.?!]{0,20}\\b${NAMED_COMPONENT_ID}\\b`)
|
|
28376
|
+
]
|
|
28377
|
+
},
|
|
28378
|
+
{
|
|
28379
|
+
// DISCOVERY/META REACH: "does <NamedFlow> relate to the <concept> concept" /
|
|
28380
|
+
// "is <A> the same as <B>" — an org-vocabulary disambiguation, which
|
|
28381
|
+
// disambiguate_concepts answers (are these two tokens the same or distinct
|
|
28382
|
+
// concepts here). Anchored on the literal "concept(s)" noun OR the "same
|
|
28383
|
+
// as … thing/idea" frame so it never grabs a compare (two components) or a
|
|
28384
|
+
// field-lineage ("where does X go") question.
|
|
28385
|
+
intent: "disambiguate-concepts-nl",
|
|
28386
|
+
plane: "vault",
|
|
28387
|
+
tools: ["sfi.disambiguate_concepts"],
|
|
28388
|
+
liveRequired: false,
|
|
28389
|
+
needsResolve: false,
|
|
28390
|
+
reason: "Whether two org-specific concepts/terms mean the same thing is a vocabulary disambiguation (disambiguate_concepts).",
|
|
28391
|
+
patterns: [
|
|
28392
|
+
/\b(?:relate\s+to|related\s+to|same\s+as|different\s+from|distinct\s+from|the\s+same\s+thing)\b[^.?!]{0,50}\bconcepts?\b/,
|
|
28393
|
+
/\bconcepts?\b[^.?!]{0,50}\b(?:relate\s+to|related\s+to|same\s+as|different\s+from|distinct\s+from)\b/,
|
|
28394
|
+
/\bis\b[^.?!]{0,30}\bthe\s+same\s+(?:as|thing\s+as|concept\s+as)\b/
|
|
28395
|
+
]
|
|
28396
|
+
},
|
|
28397
|
+
{
|
|
28398
|
+
// DISCOVERY/META REACH: "which queues does <Named>_Queue route to and who
|
|
28399
|
+
// are the MEMBERS" — a NEUTRAL single-queue inspection (no failure frame),
|
|
28400
|
+
// which get_component renders (the Queue node carries members + routing).
|
|
28401
|
+
// resolve binds the named queue first. Distinct from empty-queues above,
|
|
28402
|
+
// which only fires on a can't/stuck SYMPTOM. Anchored on a `_queue`
|
|
28403
|
+
// API-name suffix (or the literal "queue" + "members") so it never grabs a
|
|
28404
|
+
// schema-inventory ("list all queues") ask.
|
|
28405
|
+
intent: "queue-membership",
|
|
28406
|
+
plane: "vault",
|
|
28407
|
+
tools: ["sfi.resolve", "sfi.get_component"],
|
|
28408
|
+
liveRequired: false,
|
|
28409
|
+
needsResolve: true,
|
|
28410
|
+
reason: "A single queue's members and routing targets are on its Queue node \u2014 resolve the queue, then get_component.",
|
|
28411
|
+
patterns: [
|
|
28412
|
+
/\bwho\s+are\s+the\s+members?\b[^.?!]{0,60}\bqueues?\b/,
|
|
28413
|
+
/\bqueues?\b[^.?!]{0,60}\bwho\s+are\s+the\s+members?\b/,
|
|
28414
|
+
/\bwhich\s+queues?\b[^.?!]{0,40}\broute\b/
|
|
28415
|
+
]
|
|
28416
|
+
},
|
|
28417
|
+
{
|
|
28418
|
+
// DISCOVERY/META REACH: "what's the API version on the TDTM handlers / these
|
|
28419
|
+
// HEDA classes" — the per-class apiVersion is a get_component field. The
|
|
28420
|
+
// existing schema-rule api-version patterns cover the SINGULAR
|
|
28421
|
+
// "api version of X class" (Family-D contract) but missed the PLURAL
|
|
28422
|
+
// "handlers"/"classes" scan. Requires the api-version phrase with a PLURAL
|
|
28423
|
+
// handlers/classes/triggers noun (mandatory `s`) so the singular
|
|
28424
|
+
// "what is the api version of the AccountService class" stays on schema and
|
|
28425
|
+
// a plain "what version are we on" (org release) never matches.
|
|
28426
|
+
intent: "component-api-version",
|
|
28427
|
+
plane: "vault",
|
|
28428
|
+
tools: ["sfi.resolve", "sfi.list_components", "sfi.get_component"],
|
|
28429
|
+
liveRequired: false,
|
|
28430
|
+
needsResolve: true,
|
|
28431
|
+
reason: "The api version across a set of Apex classes / triggers / handlers is a per-component property scan \u2014 list_components then get_component per member.",
|
|
28432
|
+
patterns: [
|
|
28433
|
+
/\bapi\s+version\b[^.?!]{0,60}\b(?:classes|handlers|triggers)\b/,
|
|
28434
|
+
/\b(?:classes|handlers|triggers)\b[^.?!]{0,60}\bapi\s+version\b/
|
|
28435
|
+
]
|
|
28436
|
+
},
|
|
28437
|
+
{
|
|
28438
|
+
// DISCOVERY/META REACH: "explain the difference between <RT_A> and <RT_B>
|
|
28439
|
+
// and <RT_C> record types" — an enumeration of MULTIPLE record types on an
|
|
28440
|
+
// object; list_components(type: 'RecordType') is the honest surface (the
|
|
28441
|
+
// user wants each type side by side, not a pairwise diff). Requires the
|
|
28442
|
+
// literal "record types" plural AND at least the "difference"/"vs" framing
|
|
28443
|
+
// over 3+ named types joined by "and" — so a two-thing "compare A and B"
|
|
28444
|
+
// (handled by compare-components) and a single "what is the X record type"
|
|
28445
|
+
// do not land here.
|
|
28446
|
+
intent: "record-type-enumeration",
|
|
28447
|
+
plane: "vault",
|
|
28448
|
+
tools: ["sfi.resolve", "sfi.list_components", "sfi.get_component"],
|
|
28449
|
+
liveRequired: false,
|
|
28450
|
+
needsResolve: true,
|
|
28451
|
+
reason: "Distinguishing several record types on an object is a vault enumeration \u2014 list_components(RecordType) plus get_component per type.",
|
|
28452
|
+
suggestArgs: () => ({ type: "RecordType" }),
|
|
28453
|
+
patterns: [
|
|
28454
|
+
// "difference between A and B and C record types" — 3+ names joined by
|
|
28455
|
+
// "and", trailing "record types". The two "and"-joins are the tell that
|
|
28456
|
+
// this is a multi-type enumeration, not a pairwise compare.
|
|
28457
|
+
/\b(?:difference|differ|distinguish|compare)\b[^.?!]{0,120}\band\b[^.?!]{0,60}\band\b[^.?!]{0,60}\brecord\s+types?\b/
|
|
28458
|
+
]
|
|
28459
|
+
},
|
|
27479
28460
|
{
|
|
27480
28461
|
intent: "capabilities",
|
|
27481
28462
|
plane: "vault",
|
|
@@ -27487,7 +28468,21 @@ var init_intent_router = __esm({
|
|
|
27487
28468
|
/\bwhat\s+can\s+you\s+do\b/,
|
|
27488
28469
|
/\bwhat\s+are\s+you\s+capable\b/,
|
|
27489
28470
|
/\bwhat\s+tools?\s+(do\s+you\s+have|are\s+available)\b/,
|
|
27490
|
-
/\bhelp\b.*\b(capabilit|tools?|commands?)\b
|
|
28471
|
+
/\bhelp\b.*\b(capabilit|tools?|commands?)\b/,
|
|
28472
|
+
// DISCOVERY/META REACH: "what are you actually able to answer about this
|
|
28473
|
+
// org", "what can I ask" — a self-capability ask. `able to answer` /
|
|
28474
|
+
// `can .. ask` co-anchored so it never grabs an org-content question
|
|
28475
|
+
// ("what can this profile do"): the subject is the tool ("you"/"I"/"this"),
|
|
28476
|
+
// not an org component.
|
|
28477
|
+
/\bwhat\b[^.?!]{0,30}\b(?:you|i)\b[^.?!]{0,20}\b(?:able\s+to\s+answer|answer\s+about|ask\s+about|ask\s+you)\b/,
|
|
28478
|
+
/\bwhat\s+can\s+i\s+ask\b/,
|
|
28479
|
+
// "can you (even) tell me anything about … record data … or is this just
|
|
28480
|
+
// metadata" — a boundary/capability probe. `just metadata` / `only
|
|
28481
|
+
// metadata` is the capability-boundary tell (capabilities reports the
|
|
28482
|
+
// metadata-vs-live-record boundary); it never appears in a real
|
|
28483
|
+
// org-content question.
|
|
28484
|
+
/\b(?:just|only)\s+metadata\b/,
|
|
28485
|
+
/\bcan\s+you\s+(?:even\s+)?tell\s+me\s+anything\s+about\b[^.?!]{0,40}\b(?:record\s+data|data\s+in\s+here)\b/
|
|
27491
28486
|
]
|
|
27492
28487
|
},
|
|
27493
28488
|
{
|
|
@@ -35219,7 +36214,7 @@ var init_effective_permissions = __esm({
|
|
|
35219
36214
|
};
|
|
35220
36215
|
BASE_DISCLOSURES = Object.freeze([
|
|
35221
36216
|
"Permission-set GROUP membership IS expanded: a PermissionSetGroup passed in `permissionSetIds` is unioned into its member permission sets (declared metadata). Muting permission sets are DISCLOSED but NOT subtracted \u2014 a group with a muting set may confer LESS than shown.",
|
|
35222
|
-
"App and tab visibility are a separate surface (now extracted \u2014 see `app_access` / `tab_availability`); they are not part of this permission union, which composes object / field / Apex / system permissions.",
|
|
36217
|
+
"App and tab visibility are a separate surface (now extracted \u2014 see `app_access` / `tab_availability`); they are not part of this permission union, which composes object / field / Apex / system / custom permissions AND record-type visibilities (for the per-object grouped record-type view use `recordtype_availability`).",
|
|
35223
36218
|
"Field-level access is summarised here (count of fields with FLS); use `field_access_audit` for a specific field. Object permission is NOT record access \u2014 record visibility still depends on OWD + sharing (`why_cant_user_see_record`)."
|
|
35224
36219
|
]);
|
|
35225
36220
|
effectivePermissionsHandler = async (ctx, input2) => {
|
|
@@ -35260,6 +36255,8 @@ var init_effective_permissions = __esm({
|
|
|
35260
36255
|
const apexClasses = /* @__PURE__ */ new Set();
|
|
35261
36256
|
const systemPermMap = /* @__PURE__ */ new Map();
|
|
35262
36257
|
const customPermMap = /* @__PURE__ */ new Map();
|
|
36258
|
+
const rtVisMap = /* @__PURE__ */ new Map();
|
|
36259
|
+
const containersWithoutRtData = [];
|
|
35263
36260
|
const presentContainers = [];
|
|
35264
36261
|
const missingContainers = [];
|
|
35265
36262
|
for (const containerId of containers) {
|
|
@@ -35282,6 +36279,24 @@ var init_effective_permissions = __esm({
|
|
|
35282
36279
|
systemPermMap.set(p2, set);
|
|
35283
36280
|
}
|
|
35284
36281
|
}
|
|
36282
|
+
const rtRaw = nodeResult.value.properties["recordTypeVisibilities"];
|
|
36283
|
+
if (Array.isArray(rtRaw)) {
|
|
36284
|
+
for (const entry of rtRaw) {
|
|
36285
|
+
if (entry === null || typeof entry !== "object")
|
|
36286
|
+
continue;
|
|
36287
|
+
const rt2 = entry.recordType;
|
|
36288
|
+
if (typeof rt2 !== "string")
|
|
36289
|
+
continue;
|
|
36290
|
+
const accum = rtVisMap.get(rt2) ?? { visible: false, grantedBy: /* @__PURE__ */ new Set() };
|
|
36291
|
+
if (entry.visible !== false) {
|
|
36292
|
+
accum.visible = true;
|
|
36293
|
+
accum.grantedBy.add(containerId);
|
|
36294
|
+
}
|
|
36295
|
+
rtVisMap.set(rt2, accum);
|
|
36296
|
+
}
|
|
36297
|
+
} else {
|
|
36298
|
+
containersWithoutRtData.push(containerId);
|
|
36299
|
+
}
|
|
35285
36300
|
const edgesResult = await listEdges(ctx.graph, containerId, {
|
|
35286
36301
|
direction: "out",
|
|
35287
36302
|
edgeType: "grantedBy"
|
|
@@ -35352,6 +36367,11 @@ var init_effective_permissions = __esm({
|
|
|
35352
36367
|
});
|
|
35353
36368
|
}
|
|
35354
36369
|
const missingCustomPerms = customPermissions.filter((c2) => c2.targetMissing).length;
|
|
36370
|
+
const recordTypeVisibilities = [...rtVisMap.entries()].map(([recordType, a2]) => ({
|
|
36371
|
+
recordType,
|
|
36372
|
+
visible: a2.visible,
|
|
36373
|
+
grantedBy: [...a2.grantedBy].sort()
|
|
36374
|
+
})).sort((x2, y2) => x2.recordType < y2.recordType ? -1 : x2.recordType > y2.recordType ? 1 : 0);
|
|
35355
36375
|
const totalObjects = objectPermissions.length;
|
|
35356
36376
|
const limit = input2.limit ?? DEFAULT_LIMIT5;
|
|
35357
36377
|
const TOOL = "sfi.effective_permissions";
|
|
@@ -35405,18 +36425,23 @@ var init_effective_permissions = __esm({
|
|
|
35405
36425
|
if (missingCustomPerms > 0) {
|
|
35406
36426
|
disclosures.push(`${missingCustomPerms} granted custom permission(s) name a definition not present in this vault (targetMissing) \u2014 likely managed-package or not retrieved; the grant is declared but the definition is not resolvable here. Custom permissions are NOT system userPermissions, so they are not double-counted under systemPermissions.`);
|
|
35407
36427
|
}
|
|
36428
|
+
if (containersWithoutRtData.length > 0) {
|
|
36429
|
+
disclosures.push(`${containersWithoutRtData.length} container(s) carry no extracted \`recordTypeVisibilities\` property (${[...containersWithoutRtData].sort().join(", ")}) \u2014 the vault was refreshed before record-type extraction, so their record-type visibility is NOT in this union; re-run \`/sfi-refresh\`. The missing contribution is "not modeled", never a verified "no record types".`);
|
|
36430
|
+
}
|
|
35408
36431
|
return ok({
|
|
35409
36432
|
data: {
|
|
35410
36433
|
containers: presentContainers,
|
|
35411
36434
|
objectPermissions: objectPage,
|
|
35412
36435
|
systemPermissions: systemPage,
|
|
35413
36436
|
customPermissions,
|
|
36437
|
+
recordTypeVisibilities,
|
|
35414
36438
|
summary: {
|
|
35415
36439
|
objects: totalObjects,
|
|
35416
36440
|
fieldsWithFls: fieldsWithFls.size,
|
|
35417
36441
|
apexClasses: apexClasses.size,
|
|
35418
36442
|
systemPermissions: systemPermissions.length,
|
|
35419
|
-
customPermissions: customPermissions.length
|
|
36443
|
+
customPermissions: customPermissions.length,
|
|
36444
|
+
recordTypeVisibilities: recordTypeVisibilities.length
|
|
35420
36445
|
},
|
|
35421
36446
|
limit,
|
|
35422
36447
|
offset,
|
|
@@ -40698,8 +41723,9 @@ var init_field_lineage = __esm({
|
|
|
40698
41723
|
});
|
|
40699
41724
|
|
|
40700
41725
|
// ../mcp/dist/src/tools/field-mapping-between-objects.js
|
|
41726
|
+
import { basename as basename12 } from "node:path";
|
|
40701
41727
|
import { z as z51 } from "zod";
|
|
40702
|
-
var fieldMappingBetweenObjectsInputSchema, parsePropertiesJson5, loadFields2, tokenize4, jaccard, fieldTokens, TEXT_TYPES, NUMBER_TYPES, DATE_TYPES, PICKLIST_TYPES2, REFERENCE_TYPES, typeCompatible, openVault, HEURISTIC_MAPPING_DISCLOSURE, vaultNotFoundResponse2, fieldMappingBetweenObjectsHandler;
|
|
41728
|
+
var fieldMappingBetweenObjectsInputSchema, parsePropertiesJson5, loadFields2, tokenize4, jaccard, fieldTokens, TEXT_TYPES, NUMBER_TYPES, DATE_TYPES, PICKLIST_TYPES2, REFERENCE_TYPES, typeCompatible, openVault, HEURISTIC_MAPPING_DISCLOSURE, servedVaultRef, isSelfReferentialAlias, SINGLE_VAULT_NOTE, OMIT_VAULT_HINT, vaultNotFoundResponse2, fieldMappingBetweenObjectsHandler;
|
|
40703
41729
|
var init_field_mapping_between_objects = __esm({
|
|
40704
41730
|
"../mcp/dist/src/tools/field-mapping-between-objects.js"() {
|
|
40705
41731
|
"use strict";
|
|
@@ -40707,7 +41733,12 @@ var init_field_mapping_between_objects = __esm({
|
|
|
40707
41733
|
init_src2();
|
|
40708
41734
|
init_src3();
|
|
40709
41735
|
fieldMappingBetweenObjectsInputSchema = z51.object({
|
|
40710
|
-
|
|
41736
|
+
/**
|
|
41737
|
+
* Optional registered-vault alias. When omitted the tool answers from
|
|
41738
|
+
* the SERVED vault (the one this MCP session was launched against) —
|
|
41739
|
+
* the normal single-vault deployment needs no registry at all.
|
|
41740
|
+
*/
|
|
41741
|
+
vault: z51.string().min(1).optional(),
|
|
40711
41742
|
objectA: z51.string().min(1),
|
|
40712
41743
|
objectB: z51.string().min(1),
|
|
40713
41744
|
similarityThreshold: z51.number().min(0).max(1).optional(),
|
|
@@ -40815,8 +41846,22 @@ var init_field_mapping_between_objects = __esm({
|
|
|
40815
41846
|
return ok({ store, dispose: async () => closeGraph(store) });
|
|
40816
41847
|
};
|
|
40817
41848
|
HEURISTIC_MAPPING_DISCLOSURE = "field-mapping suggestions are heuristic \u2014 labels are matched by token overlap and types by compatibility table. Verify each suggested pair against your business rules before relying on the mapping for a migration script.";
|
|
40818
|
-
|
|
40819
|
-
|
|
41849
|
+
servedVaultRef = (ctx) => ({
|
|
41850
|
+
alias: basename12(ctx.vaultRoot),
|
|
41851
|
+
path: ctx.vaultRoot,
|
|
41852
|
+
registeredAt: "",
|
|
41853
|
+
lastRefreshedAt: ctx.manifest.refreshedAt,
|
|
41854
|
+
sourceTreeHash: ctx.manifest.sourceTreeHash,
|
|
41855
|
+
componentCount: null
|
|
41856
|
+
});
|
|
41857
|
+
isSelfReferentialAlias = (alias, vaultRoot) => {
|
|
41858
|
+
const a2 = alias.toLowerCase();
|
|
41859
|
+
return a2 === vaultRoot.toLowerCase() || a2 === basename12(vaultRoot).toLowerCase();
|
|
41860
|
+
};
|
|
41861
|
+
SINGLE_VAULT_NOTE = "No multi-vault registry found (set SF_INTELLIGENCE_REGISTRY_PATH to enable). This looks like a single-vault install \u2014 answered from the served vault.";
|
|
41862
|
+
OMIT_VAULT_HINT = "Omit `vault` to map fields within the served vault.";
|
|
41863
|
+
vaultNotFoundResponse2 = (vault, objectA, objectB, ctx, extraBoundaries = []) => {
|
|
41864
|
+
const message = `vault alias '${vault}' is not registered. Run \`sfi register-vault ${vault} <path>\` first, or \`sfi list-vaults\` to see what's registered. ${OMIT_VAULT_HINT}`;
|
|
40820
41865
|
return ok({
|
|
40821
41866
|
data: {
|
|
40822
41867
|
vault: {
|
|
@@ -40832,7 +41877,7 @@ var init_field_mapping_between_objects = __esm({
|
|
|
40832
41877
|
suggestedPairs: [],
|
|
40833
41878
|
unpairedFromA: [],
|
|
40834
41879
|
unpairedFromB: [],
|
|
40835
|
-
boundaries: [HEURISTIC_MAPPING_DISCLOSURE, message]
|
|
41880
|
+
boundaries: [HEURISTIC_MAPPING_DISCLOSURE, message, ...extraBoundaries]
|
|
40836
41881
|
},
|
|
40837
41882
|
vaultState: {
|
|
40838
41883
|
sourceTreeHash: ctx.manifest.sourceTreeHash,
|
|
@@ -40844,22 +41889,40 @@ var init_field_mapping_between_objects = __esm({
|
|
|
40844
41889
|
const registryRoot = findRegistryRoot(ctx.vaultRoot);
|
|
40845
41890
|
const threshold = input2.similarityThreshold ?? 0.5;
|
|
40846
41891
|
const includeTypeIncompatible = input2.includeTypeIncompatible === true;
|
|
40847
|
-
|
|
40848
|
-
|
|
40849
|
-
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
|
|
40856
|
-
|
|
40857
|
-
|
|
40858
|
-
|
|
40859
|
-
|
|
40860
|
-
|
|
41892
|
+
let vaultPath;
|
|
41893
|
+
let vaultRef;
|
|
41894
|
+
const extraBoundaries = [];
|
|
41895
|
+
if (input2.vault === void 0) {
|
|
41896
|
+
vaultPath = ctx.vaultRoot;
|
|
41897
|
+
vaultRef = servedVaultRef(ctx);
|
|
41898
|
+
} else {
|
|
41899
|
+
const registry = await loadRegistry(registryRoot);
|
|
41900
|
+
if (!registry.ok && registry.error.kind === "registry-missing") {
|
|
41901
|
+
if (!isSelfReferentialAlias(input2.vault, ctx.vaultRoot)) {
|
|
41902
|
+
return vaultNotFoundResponse2(input2.vault, input2.objectA, input2.objectB, ctx, [
|
|
41903
|
+
"No multi-vault registry found (set SF_INTELLIGENCE_REGISTRY_PATH to enable). This looks like a single-vault install \u2014 omit `vault` to map fields within the served vault."
|
|
41904
|
+
]);
|
|
41905
|
+
}
|
|
41906
|
+
vaultPath = ctx.vaultRoot;
|
|
41907
|
+
vaultRef = { ...servedVaultRef(ctx), alias: input2.vault };
|
|
41908
|
+
extraBoundaries.push(SINGLE_VAULT_NOTE);
|
|
41909
|
+
} else {
|
|
41910
|
+
const pathResult = await resolveVault(registryRoot, input2.vault);
|
|
41911
|
+
if (!pathResult.ok) {
|
|
41912
|
+
return vaultNotFoundResponse2(input2.vault, input2.objectA, input2.objectB, ctx);
|
|
41913
|
+
}
|
|
41914
|
+
const vaultRefResult = await getVaultRef(registryRoot, input2.vault);
|
|
41915
|
+
if (!vaultRefResult.ok) {
|
|
41916
|
+
return err({
|
|
41917
|
+
kind: "internal",
|
|
41918
|
+
message: "failed to load vault metadata after alias resolution"
|
|
41919
|
+
});
|
|
41920
|
+
}
|
|
41921
|
+
vaultPath = pathResult.value;
|
|
41922
|
+
vaultRef = vaultRefResult.value;
|
|
41923
|
+
}
|
|
40861
41924
|
}
|
|
40862
|
-
const opened = await openVault(ctx,
|
|
41925
|
+
const opened = await openVault(ctx, vaultPath);
|
|
40863
41926
|
if (!opened.ok)
|
|
40864
41927
|
return opened;
|
|
40865
41928
|
try {
|
|
@@ -40901,13 +41964,13 @@ var init_field_mapping_between_objects = __esm({
|
|
|
40901
41964
|
const unpairedFromB = fieldsB.filter((f2) => !claimedB.has(f2.fieldApiName)).map((f2) => f2.fieldApiName).sort();
|
|
40902
41965
|
return ok({
|
|
40903
41966
|
data: {
|
|
40904
|
-
vault:
|
|
41967
|
+
vault: vaultRef,
|
|
40905
41968
|
objectA: { apiName: input2.objectA, fieldCount: fieldsA.length },
|
|
40906
41969
|
objectB: { apiName: input2.objectB, fieldCount: fieldsB.length },
|
|
40907
41970
|
suggestedPairs,
|
|
40908
41971
|
unpairedFromA,
|
|
40909
41972
|
unpairedFromB,
|
|
40910
|
-
boundaries: [HEURISTIC_MAPPING_DISCLOSURE]
|
|
41973
|
+
boundaries: [HEURISTIC_MAPPING_DISCLOSURE, ...extraBoundaries]
|
|
40911
41974
|
},
|
|
40912
41975
|
vaultState: {
|
|
40913
41976
|
sourceTreeHash: ctx.manifest.sourceTreeHash,
|
|
@@ -42275,7 +43338,9 @@ var init_find_component_usages = __esm({
|
|
|
42275
43338
|
if (!edgesRes.ok) {
|
|
42276
43339
|
return err({ kind: "internal", message: `graph query failed: ${edgesRes.error.message}` });
|
|
42277
43340
|
}
|
|
42278
|
-
const
|
|
43341
|
+
const incomingEdges = edgesRes.value;
|
|
43342
|
+
const usageEdges = incomingEdges.filter((e2) => !NON_USAGE_EDGE_TYPES.has(e2.edgeType));
|
|
43343
|
+
const grantEdges = incomingEdges.filter((e2) => e2.edgeType === "grantedBy");
|
|
42279
43344
|
const byType = /* @__PURE__ */ new Map();
|
|
42280
43345
|
for (const e2 of usageEdges) {
|
|
42281
43346
|
const rt2 = typeOf(e2.fromId);
|
|
@@ -42322,7 +43387,16 @@ var init_find_component_usages = __esm({
|
|
|
42322
43387
|
}
|
|
42323
43388
|
}
|
|
42324
43389
|
const hasStaticEvidence = graphReferrerCount > 0 || grepMatches.length > 0;
|
|
42325
|
-
|
|
43390
|
+
const surfaceGrants = targetType === "CustomPermission" || usageEdges.length === 0 && grantEdges.length > 0;
|
|
43391
|
+
let grantedBySection;
|
|
43392
|
+
if (surfaceGrants) {
|
|
43393
|
+
const granterIds = [...new Set(grantEdges.map((e2) => e2.fromId))].sort();
|
|
43394
|
+
grantedBySection = {
|
|
43395
|
+
count: granterIds.length,
|
|
43396
|
+
granters: granterIds.slice(0, GRAPH_REFERRER_SAMPLE).map((id) => ({ id, type: typeOf(id) }))
|
|
43397
|
+
};
|
|
43398
|
+
}
|
|
43399
|
+
if (!retrieved && graphReferrerCount === 0 && !hasStaticEvidence && grantEdges.length === 0) {
|
|
42326
43400
|
return err({
|
|
42327
43401
|
kind: "component-not-found",
|
|
42328
43402
|
message: `no component or referrer matches \`${componentId}\` in this vault`,
|
|
@@ -42345,6 +43419,9 @@ var init_find_component_usages = __esm({
|
|
|
42345
43419
|
if (GREP_RELIANT_PREFIXES.has(targetType)) {
|
|
42346
43420
|
boundaries.push(`${targetType} usage has a weaker graph tier \u2014 FRONTEND references ($Label / $Resource / $Setup and @salesforce imports in LWC/Aura/Visualforce) are modeled as graph edges on vaults refreshed at 0.1.10+, but Apex references (System.Label.X, dynamic config reads) are still grep-only, so the grep supplement carries part of the answer here. Confirm by reading the matched source.`);
|
|
42347
43421
|
}
|
|
43422
|
+
if (grantedBySection !== void 0) {
|
|
43423
|
+
boundaries.push(`Access grants are listed SEPARATELY in \`grantedBy\` (${grantedBySection.count} granting container(s)) \u2014 a grant is ACCESS, not usage, so granters never appear in graphReferrers. For a CustomPermission this answers "which Profiles / PermissionSets grant it?"; checking a custom permission in code (FeatureManagement / $Permission) is usage and stays in the usage tiers.`);
|
|
43424
|
+
}
|
|
42348
43425
|
if (!retrieved) {
|
|
42349
43426
|
boundaries.push("This component is a PHANTOM \u2014 referenced by the edges below but NOT retrieved into the vault, so its own definition is unavailable; the referrer list is still valid.");
|
|
42350
43427
|
}
|
|
@@ -42352,6 +43429,7 @@ var init_find_component_usages = __esm({
|
|
|
42352
43429
|
data: {
|
|
42353
43430
|
target: { componentId, type: targetType, apiName: targetApiName, retrieved },
|
|
42354
43431
|
graphReferrers,
|
|
43432
|
+
...grantedBySection !== void 0 ? { grantedBy: grantedBySection } : {},
|
|
42355
43433
|
grepSupplement: {
|
|
42356
43434
|
tier: "text-match",
|
|
42357
43435
|
ran: grepRan,
|
|
@@ -43306,7 +44384,7 @@ var init_find_formula_references = __esm({
|
|
|
43306
44384
|
|
|
43307
44385
|
// ../mcp/dist/src/tools/find-hardcoded-values-anywhere.js
|
|
43308
44386
|
import { readFile as readFile74 } from "node:fs/promises";
|
|
43309
|
-
import { basename as
|
|
44387
|
+
import { basename as basename13 } from "node:path";
|
|
43310
44388
|
import { z as z63 } from "zod";
|
|
43311
44389
|
var MAX_LIMIT9, DEFAULT_LIMIT9, FIND_HARDCODED_ANYWHERE_TOOL, SALESFORCE_ID_REGEX, EMAIL_REGEX, DATE_REGEX, NUMERIC_REGEX, HARDCODED_APEX_RULES, APEX_SOURCE_SUFFIXES, scanApexSourceForEmails, NUMERIC_FP_DISCLOSURE, ID_FP_DISCLOSURE, TEST_CLASS_REFUSAL_DISCLOSURE, APEX_SOURCE_EMAIL_SCAN_DISCLOSURE, findHardcodedValuesAnywhereInputBaseSchema, findHardcodedValuesAnywhereInputSchema, RULE_TO_CATEGORY, coerceIssue4, regexForCategory, snippetAround, scanText, isTestClass, compareMatches, matchKey, findHardcodedValuesAnywhereHandler;
|
|
43312
44390
|
var init_find_hardcoded_values_anywhere = __esm({
|
|
@@ -43349,7 +44427,7 @@ var init_find_hardcoded_values_anywhere = __esm({
|
|
|
43349
44427
|
} catch {
|
|
43350
44428
|
continue;
|
|
43351
44429
|
}
|
|
43352
|
-
const name =
|
|
44430
|
+
const name = basename13(file.absolutePath);
|
|
43353
44431
|
const apiName = name.endsWith(".trigger") ? name.slice(0, -".trigger".length) : name.slice(0, -".cls".length);
|
|
43354
44432
|
const componentType = name.endsWith(".trigger") ? "ApexTrigger" : "ApexClass";
|
|
43355
44433
|
const componentId = `${componentType}:${apiName}`;
|
|
@@ -50063,7 +51141,7 @@ var init_lightning_pages = __esm({
|
|
|
50063
51141
|
|
|
50064
51142
|
// ../mcp/dist/src/tools/list-components.js
|
|
50065
51143
|
import { z as z90 } from "zod";
|
|
50066
|
-
var LIST_COMPONENTS_TOOL, STANDARD_OBJECT_API_NAMES2, objectApiNameFromParentId, isStandardObjectApiName, COMPONENT_TYPES2, LIST_MAX_LIMIT2, LIST_DEFAULT_LIMIT2, LIST_PAYLOAD_BUDGET_BYTES, fitNodesToBudget, APEX_BOOLEAN_FILTERS, coercedOptionalBoolean, listComponentsInputSchema, listComponentsHandler;
|
|
51144
|
+
var LIST_COMPONENTS_TOOL, STANDARD_OBJECT_API_NAMES2, objectApiNameFromParentId, isStandardObjectApiName, COMPONENT_TYPES2, LIST_MAX_LIMIT2, LIST_DEFAULT_LIMIT2, LIST_PAYLOAD_BUDGET_BYTES, ITEM_SLIM_THRESHOLD_BYTES, SLIM_STRING_MAX_CHARS, slimOversizedNode, fitNodesToBudget, APEX_BOOLEAN_FILTERS, coercedOptionalBoolean, listComponentsInputSchema, listComponentsHandler;
|
|
50067
51145
|
var init_list_components = __esm({
|
|
50068
51146
|
"../mcp/dist/src/tools/list-components.js"() {
|
|
50069
51147
|
"use strict";
|
|
@@ -50172,6 +51250,21 @@ var init_list_components = __esm({
|
|
|
50172
51250
|
LIST_MAX_LIMIT2 = 500;
|
|
50173
51251
|
LIST_DEFAULT_LIMIT2 = 50;
|
|
50174
51252
|
LIST_PAYLOAD_BUDGET_BYTES = 38e3;
|
|
51253
|
+
ITEM_SLIM_THRESHOLD_BYTES = 2048;
|
|
51254
|
+
SLIM_STRING_MAX_CHARS = 256;
|
|
51255
|
+
slimOversizedNode = (node) => {
|
|
51256
|
+
if (Buffer.byteLength(JSON.stringify(node), "utf8") <= ITEM_SLIM_THRESHOLD_BYTES) {
|
|
51257
|
+
return node;
|
|
51258
|
+
}
|
|
51259
|
+
const compact = {};
|
|
51260
|
+
for (const [key, value] of Object.entries(node.properties ?? {})) {
|
|
51261
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string" && value.length <= SLIM_STRING_MAX_CHARS) {
|
|
51262
|
+
compact[key] = value;
|
|
51263
|
+
}
|
|
51264
|
+
}
|
|
51265
|
+
compact["propertiesTruncated"] = true;
|
|
51266
|
+
return { ...node, properties: compact };
|
|
51267
|
+
};
|
|
50175
51268
|
fitNodesToBudget = (nodes, budgetBytes) => {
|
|
50176
51269
|
const kept = [];
|
|
50177
51270
|
let used = 0;
|
|
@@ -50299,7 +51392,9 @@ var init_list_components = __esm({
|
|
|
50299
51392
|
}
|
|
50300
51393
|
}
|
|
50301
51394
|
}
|
|
50302
|
-
const
|
|
51395
|
+
const slimmedPageNodes = pageNodes.map(slimOversizedNode);
|
|
51396
|
+
const { kept, trimmed } = fitNodesToBudget(slimmedPageNodes, LIST_PAYLOAD_BUDGET_BYTES);
|
|
51397
|
+
const propertiesSlimmed = kept.some((n2) => n2.properties?.["propertiesTruncated"] === true);
|
|
50303
51398
|
const hasMore = pageNodes.length === limit || trimmed;
|
|
50304
51399
|
let retrievalHint;
|
|
50305
51400
|
if (offset === 0 && pageNodes.length === 0 && !hasPropertyFilter && !hasStringPropertyFilter && !recordTriggered) {
|
|
@@ -50364,6 +51459,7 @@ var init_list_components = __esm({
|
|
|
50364
51459
|
...docFallbackNote !== void 0 ? { docFallbackNote } : {},
|
|
50365
51460
|
...coverageCaveat !== void 0 ? { coverageCaveat } : {},
|
|
50366
51461
|
...formulaFieldCount !== void 0 ? { formulaFieldCount } : {},
|
|
51462
|
+
...propertiesSlimmed ? { propertiesSlimmed: true } : {},
|
|
50367
51463
|
...trimmed ? {
|
|
50368
51464
|
truncated: true,
|
|
50369
51465
|
note: `Response trimmed to ${kept.length} of ${pageNodes.length} fetched rows to stay under the ~45 KB MCP response limit. Use totalCount (${totalCount}) for the authoritative vault count; advance with offset += ${kept.length} (or narrow via parentId) for the rest.`
|
|
@@ -54410,7 +55506,7 @@ import { createHash as createHash7 } from "node:crypto";
|
|
|
54410
55506
|
import { readFile as readFile82 } from "node:fs/promises";
|
|
54411
55507
|
import { join as join23, resolve as resolve3 } from "node:path";
|
|
54412
55508
|
import { z as z115 } from "zod";
|
|
54413
|
-
var RESOLVE_FALLBACK_MAX_TOKENS, tryResolveFallback, routeQuestionInputSchema, routeTrust, clarificationIdFor, selectedEntityArgsForRoute, extractEntityQuery, inferEntityTypes, refineEntityResolution, applyGlossaryAliases, entityAmbiguityRequiresClarification, splitCompoundQuestion, mixedInventoryAndStoragePlan, PLAN_FAMILY, ASSESSMENT_FAMILY, ROUTE_PREAMBLE_TOOLS, REGEX_BONUS, resolveActiveVaultAlias, buildRouteToolArgsMap, mergeRouteHintsIntoCandidates, invokeFromArgsMap, rerankForMode, MARGIN, DESTRUCTIVE_TOOL, INFORMATIONAL_IMPACT_TOOL, planesDiverge, risksDiverge, marginClarification, LIVE_DISCLOSURE_LOOKAHEAD, liveConsentDisclosure, guidanceForMode, buildFunnelCandidates, routerMode, routeQuestionHandler;
|
|
55509
|
+
var RESOLVE_FALLBACK_MAX_TOKENS, SAVE_ORDER_INTENTS, SINGLE_ENTITY_INTENTS, COMPARISON_ASIDE, stripComparisonAside, tryResolveFallback, routeQuestionInputSchema, routeTrust, clarificationIdFor, selectedEntityArgsForRoute, extractEntityQuery, inferEntityTypes, refineEntityResolution, applyGlossaryAliases, entityAmbiguityRequiresClarification, splitCompoundQuestion, mixedInventoryAndStoragePlan, PLAN_FAMILY, ASSESSMENT_FAMILY, ROUTE_PREAMBLE_TOOLS, TOOL_COMPATIBLE_TYPES, ACCESS_FLAVORED_TOOLS, resolvedTypeForGuard, applyComponentTypeGuard, REGEX_BONUS, resolveActiveVaultAlias, buildRouteToolArgsMap, mergeRouteHintsIntoCandidates, invokeFromArgsMap, rerankForMode, MARGIN, DESTRUCTIVE_TOOL, INFORMATIONAL_IMPACT_TOOL, planesDiverge, risksDiverge, marginClarification, LIVE_DISCLOSURE_LOOKAHEAD, liveConsentDisclosure, guidanceForMode, buildFunnelCandidates, routerMode, routeQuestionHandler;
|
|
54414
55510
|
var init_route_question = __esm({
|
|
54415
55511
|
"../mcp/dist/src/tools/route-question.js"() {
|
|
54416
55512
|
"use strict";
|
|
@@ -54423,6 +55519,24 @@ var init_route_question = __esm({
|
|
|
54423
55519
|
init_resolve2();
|
|
54424
55520
|
init_tool_profile();
|
|
54425
55521
|
RESOLVE_FALLBACK_MAX_TOKENS = 3;
|
|
55522
|
+
SAVE_ORDER_INTENTS = /* @__PURE__ */ new Set([
|
|
55523
|
+
"trigger-order",
|
|
55524
|
+
"save-behavior",
|
|
55525
|
+
"automation-on-object",
|
|
55526
|
+
"dlrs-recursion"
|
|
55527
|
+
]);
|
|
55528
|
+
SINGLE_ENTITY_INTENTS = /* @__PURE__ */ new Set([
|
|
55529
|
+
"explain-flow",
|
|
55530
|
+
"explain-apex",
|
|
55531
|
+
"get-component"
|
|
55532
|
+
]);
|
|
55533
|
+
COMPARISON_ASIDE = new RegExp("\\s*(?:,|\u2014|-|;)?\\s*\\b(?:is\\s+(?:it|this|that)\\s+the\\s+same\\s+as|is\\s+(?:it|this|that)\\s+different\\s+(?:from|to)|(?:the\\s+)?same\\s+as|compared\\s+to|or\\s+is\\s+(?:it|this|that)|vs\\.?|versus)\\b.*$", "i");
|
|
55534
|
+
stripComparisonAside = (question, intent) => {
|
|
55535
|
+
if (!SINGLE_ENTITY_INTENTS.has(intent))
|
|
55536
|
+
return question;
|
|
55537
|
+
const stripped = question.replace(COMPARISON_ASIDE, "").trim();
|
|
55538
|
+
return stripped.length > 0 ? stripped : question;
|
|
55539
|
+
};
|
|
54426
55540
|
tryResolveFallback = async (ctx, question) => {
|
|
54427
55541
|
const tokens = question.trim().split(/\s+/).filter((t2) => t2.length > 0);
|
|
54428
55542
|
if (tokens.length === 0 || tokens.length > RESOLVE_FALLBACK_MAX_TOKENS) {
|
|
@@ -54536,23 +55650,29 @@ var init_route_question = __esm({
|
|
|
54536
55650
|
if (intent === "what-if-method-signature" && apiReference.includes(".")) {
|
|
54537
55651
|
return `${apiReference.slice(0, apiReference.indexOf("."))} class`;
|
|
54538
55652
|
}
|
|
55653
|
+
if (!apiReference.includes(".")) {
|
|
55654
|
+
const qualifier = question.match(new RegExp(`\\b${apiReference}\\b\\s+(?:field\\s+)?on\\s+(?:the\\s+)?([A-Za-z][A-Za-z0-9_]*)\\b`, "i"))?.[1];
|
|
55655
|
+
if (qualifier !== void 0)
|
|
55656
|
+
return `${qualifier}.${apiReference}`;
|
|
55657
|
+
}
|
|
54539
55658
|
return apiReference;
|
|
54540
55659
|
}
|
|
54541
55660
|
const prefixedTypePhrase = question.match(/\b((?:validation\s+rule|permission\s+set|record\s+type|page\s+layout|object|field|flow|class|trigger|layout|profile|report|dashboard)\s+(?:named\s+)?[A-Z][A-Za-z0-9_]*(?:\s+[A-Z][A-Za-z0-9_]*){0,5})\b/)?.[1];
|
|
54542
55661
|
if (prefixedTypePhrase !== void 0)
|
|
54543
55662
|
return prefixedTypePhrase.trim();
|
|
54544
|
-
const typedMatch = question.match(/\b(?:the\s+)?([A-Za-z][A-Za-z0-9_]*(?:[\s_-]+[A-Za-z][A-Za-z0-9_]*){0,5}\s+(?:object|field|flow|class|trigger|layout|profile|permission\s+set|record\s+type|validation\s+rule|report|dashboard))(?:\s+(?:on|for|of)\s+(?:the\s+)?([A-Za-z][A-Za-z0-9_]*)(?:\s+object)?)?\b/i);
|
|
55663
|
+
const typedMatch = question.match(/\b(?:the\s+)?([A-Za-z][A-Za-z0-9_]*(?:[\s_-]+[A-Za-z][A-Za-z0-9_]*){0,5}\s+(?:object|field|flow|class|trigger|layout|profile|permission\s+set|record\s+type|validation\s+rule|report|dashboard|logic))(?:\s+(?:on|for|of)\s+(?:the\s+)?([A-Za-z][A-Za-z0-9_]*)(?:\s+object)?)?\b/i);
|
|
54545
55664
|
const typedPhrase = typedMatch?.[1];
|
|
54546
55665
|
if (typedPhrase === void 0)
|
|
54547
55666
|
return null;
|
|
54548
|
-
const cleaned = typedPhrase.replace(/^(?:(?:what|which|who|where|when|why|how|is|are|can|does|do|did|show|find|explain|locate|list|references?|owns?|edit|read|view|access|change|delete|remove|possible|values?|api|version|data|type|for|of|the|this|that|in|on|to|used|assigned|set)\s+)+/i, "").trim();
|
|
54549
|
-
if (/\b(?:and|when|should|use|there|any|that|no|available|tools?|required\s+fields?|record\s+types?)\b/i.test(cleaned))
|
|
55667
|
+
const cleaned = typedPhrase.replace(/^(?:(?:what|whatever|which|who|where|when|why|how|is|are|can|does|do|did|show|find|explain|locate|list|calls?|invokes?|references?|owns?|edit|read|view|access|change|delete|remove|possible|values?|apex|api|version|data|type|for|of|the|this|that|in|on|to|used|assigned|set|every|all|each|any|me|a|an)\s+)+/i, "").trim();
|
|
55668
|
+
if (/\b(?:and|when|should|use|there|any|that|no|available|tools?|difference|between|required\s+fields?|record\s+types?)\b/i.test(cleaned))
|
|
54550
55669
|
return null;
|
|
54551
|
-
const distinctive = cleaned.replace(/\b(?:object|field|flow|class|trigger|page|layout|profile|permission|set|record|type|validation|rule|report|dashboard|data)\b/gi, "").replace(/[^A-Za-z0-9_]+/g, "");
|
|
55670
|
+
const distinctive = cleaned.replace(/\b(?:object|field|flow|class|trigger|page|layout|profile|permission|set|record|type|validation|rule|report|dashboard|data|logic)\b/gi, "").replace(/[^A-Za-z0-9_]+/g, "");
|
|
54552
55671
|
if (distinctive.length === 0)
|
|
54553
55672
|
return null;
|
|
55673
|
+
const phrase = cleaned.replace(/\s+logic$/i, "");
|
|
54554
55674
|
const parent = typedMatch?.[2];
|
|
54555
|
-
return parent === void 0 ?
|
|
55675
|
+
return parent === void 0 ? phrase : `${phrase} on ${parent}`;
|
|
54556
55676
|
};
|
|
54557
55677
|
inferEntityTypes = (query, intent, question) => {
|
|
54558
55678
|
if (intent === "what-if-method-signature")
|
|
@@ -54692,6 +55812,54 @@ var init_route_question = __esm({
|
|
|
54692
55812
|
PLAN_FAMILY = /^sfi\.(what_if_|get_impact|safe_to_delete|downstream_effects|tests_for_change|field_lineage)/;
|
|
54693
55813
|
ASSESSMENT_FAMILY = /(_risk_report$|^sfi\.release_readiness|^sfi\.promotion_readiness|^sfi\.coverage_report|^sfi\.tech_debt_score|^sfi\.governor_limit_risks|^sfi\.crud_fls_audit)/;
|
|
54694
55814
|
ROUTE_PREAMBLE_TOOLS = /* @__PURE__ */ new Set(["sfi.resolve", "sfi.capabilities"]);
|
|
55815
|
+
TOOL_COMPATIBLE_TYPES = /* @__PURE__ */ new Map([
|
|
55816
|
+
["sfi.call_graph", /* @__PURE__ */ new Set(["ApexClass", "ApexTrigger"])],
|
|
55817
|
+
["sfi.method_reachability", /* @__PURE__ */ new Set(["ApexClass", "ApexTrigger"])],
|
|
55818
|
+
["sfi.explain_apex_method", /* @__PURE__ */ new Set(["ApexClass", "ApexTrigger"])],
|
|
55819
|
+
["sfi.who_can_access_object", /* @__PURE__ */ new Set(["CustomObject"])],
|
|
55820
|
+
["sfi.object_access_audit", /* @__PURE__ */ new Set(["CustomObject"])],
|
|
55821
|
+
["sfi.field_access_audit", /* @__PURE__ */ new Set(["CustomField"])]
|
|
55822
|
+
]);
|
|
55823
|
+
ACCESS_FLAVORED_TOOLS = /* @__PURE__ */ new Set([
|
|
55824
|
+
"sfi.who_can_access_object",
|
|
55825
|
+
"sfi.object_access_audit",
|
|
55826
|
+
"sfi.field_access_audit"
|
|
55827
|
+
]);
|
|
55828
|
+
resolvedTypeForGuard = (route, resolution) => {
|
|
55829
|
+
if (resolution === null || resolution.candidates.length === 0)
|
|
55830
|
+
return null;
|
|
55831
|
+
const top = resolution.candidates[0];
|
|
55832
|
+
if (resolution.disposition === "exact")
|
|
55833
|
+
return top.type;
|
|
55834
|
+
if (resolution.disposition !== "ambiguous")
|
|
55835
|
+
return null;
|
|
55836
|
+
const guardedCompatibleTypes = new Set(route.tools.flatMap((tool) => [...TOOL_COMPATIBLE_TYPES.get(tool) ?? []]));
|
|
55837
|
+
if (guardedCompatibleTypes.size === 0)
|
|
55838
|
+
return null;
|
|
55839
|
+
return resolution.candidates.some((candidate) => guardedCompatibleTypes.has(candidate.type)) ? null : top.type;
|
|
55840
|
+
};
|
|
55841
|
+
applyComponentTypeGuard = (route, resolvedType) => {
|
|
55842
|
+
if (resolvedType !== "Flow")
|
|
55843
|
+
return route;
|
|
55844
|
+
const incompatible = new Set(route.tools.filter((tool) => {
|
|
55845
|
+
const compatible = TOOL_COMPATIBLE_TYPES.get(tool);
|
|
55846
|
+
return compatible !== void 0 && !compatible.has(resolvedType);
|
|
55847
|
+
}));
|
|
55848
|
+
if (incompatible.size === 0)
|
|
55849
|
+
return route;
|
|
55850
|
+
const substitutes = [...incompatible].some((tool) => ACCESS_FLAVORED_TOOLS.has(tool)) ? ["sfi.who_can_run", "sfi.explain_flow", "sfi.get_impact"] : ["sfi.explain_flow", "sfi.get_impact"];
|
|
55851
|
+
const substituteSet = new Set(substitutes);
|
|
55852
|
+
const preamble = route.tools.filter((tool) => ROUTE_PREAMBLE_TOOLS.has(tool));
|
|
55853
|
+
const kept = route.tools.filter((tool) => !ROUTE_PREAMBLE_TOOLS.has(tool) && !incompatible.has(tool) && !substituteSet.has(tool));
|
|
55854
|
+
const originalTools = route.tools;
|
|
55855
|
+
const tools = [...preamble, ...substitutes, ...kept];
|
|
55856
|
+
return {
|
|
55857
|
+
...route,
|
|
55858
|
+
tools,
|
|
55859
|
+
reason: `${route.reason} The named entity resolved to a Flow, so tools that require an Apex class, object, or field id were replaced with the Flow-appropriate ones.`,
|
|
55860
|
+
plan: route.plan.map((step3) => step3.tools === originalTools ? { ...step3, tools } : step3)
|
|
55861
|
+
};
|
|
55862
|
+
};
|
|
54695
55863
|
REGEX_BONUS = 0.25;
|
|
54696
55864
|
resolveActiveVaultAlias = async (ctx) => {
|
|
54697
55865
|
const normalizedRoot = resolve3(ctx.vaultRoot);
|
|
@@ -54902,8 +56070,11 @@ var init_route_question = __esm({
|
|
|
54902
56070
|
suggestedArgs: { type: "CustomObject" }
|
|
54903
56071
|
};
|
|
54904
56072
|
}
|
|
54905
|
-
const
|
|
54906
|
-
const
|
|
56073
|
+
const saveOrderIntent = SAVE_ORDER_INTENTS.has(route.intent);
|
|
56074
|
+
const suggestedObject = route.suggestedArgs?.["objectApiName"];
|
|
56075
|
+
const entityExtractionSource = stripComparisonAside(input2.question, route.intent);
|
|
56076
|
+
const entityQuery = !route.needsResolve ? null : saveOrderIntent ? typeof suggestedObject === "string" ? suggestedObject : null : extractEntityQuery(entityExtractionSource, route.intent);
|
|
56077
|
+
const entityTypes = entityQuery === null ? [] : saveOrderIntent ? ["CustomObject"] : inferEntityTypes(entityQuery, route.intent, input2.question);
|
|
54907
56078
|
const entityResolution = entityQuery !== null ? await resolveComponents(ctx.graph, entityQuery, {
|
|
54908
56079
|
limit: 5,
|
|
54909
56080
|
...entityTypes.length > 0 ? { types: entityTypes } : {}
|
|
@@ -54942,6 +56113,7 @@ var init_route_question = __esm({
|
|
|
54942
56113
|
} else if (entityEvidence?.disposition === "ambiguous" && route.clarification === null) {
|
|
54943
56114
|
route = { ...route, confidence: "medium" };
|
|
54944
56115
|
}
|
|
56116
|
+
route = applyComponentTypeGuard(route, resolvedTypeForGuard(route, refinedEntityResolution));
|
|
54945
56117
|
const marginRouteToolArgs = await buildRouteToolArgsMap(route, ctx);
|
|
54946
56118
|
if (routerMode() === "hybrid" && route.confidence !== "high") {
|
|
54947
56119
|
const gateCandidates = buildFunnelCandidates(route, input2.question, marginRouteToolArgs, input2.mode);
|
|
@@ -54950,6 +56122,59 @@ var init_route_question = __esm({
|
|
|
54950
56122
|
route = { ...route, confidence: "low", clarification: marginClar };
|
|
54951
56123
|
}
|
|
54952
56124
|
}
|
|
56125
|
+
if (entityQuery !== null && refinedEntityResolution !== null && refinedEntityResolution.disposition === "none") {
|
|
56126
|
+
const bareQuery = entityQuery.replace(/^(?:the|a|an)\s+/i, "").replace(/\s+(?:class(?:es)?|trigger(?:s)?|field(?:s)?|object(?:s)?|flow(?:s)?|component(?:s)?|layout(?:s)?|profile(?:s)?|report(?:s)?|dashboard(?:s)?|rule(?:s)?)$/i, "");
|
|
56127
|
+
const bareRetry = bareQuery !== entityQuery && bareQuery.length > 0 ? await resolveComponents(ctx.graph, bareQuery, {
|
|
56128
|
+
limit: 5,
|
|
56129
|
+
...entityTypes.length > 0 ? { types: entityTypes } : {}
|
|
56130
|
+
}) : null;
|
|
56131
|
+
const bareResolution = bareRetry?.ok === true && bareRetry.value.disposition !== "none" ? bareRetry.value : null;
|
|
56132
|
+
if (bareResolution !== null) {
|
|
56133
|
+
entityEvidence = {
|
|
56134
|
+
query: bareQuery,
|
|
56135
|
+
typeHints: entityTypes,
|
|
56136
|
+
disposition: bareResolution.disposition,
|
|
56137
|
+
clarificationRequired: false,
|
|
56138
|
+
warning: bareResolution.disposition === "ambiguous" ? "Possible component matches were found, but none is strong enough to interrupt routing. Resolve the component before executing a component-specific analysis." : null,
|
|
56139
|
+
candidates: bareResolution.candidates.slice(0, 5).map((candidate) => ({
|
|
56140
|
+
componentId: candidate.id,
|
|
56141
|
+
type: candidate.type,
|
|
56142
|
+
apiName: candidate.apiName,
|
|
56143
|
+
label: candidate.label,
|
|
56144
|
+
parentApiName: candidate.parentApiName,
|
|
56145
|
+
score: candidate.score,
|
|
56146
|
+
base: candidate.base,
|
|
56147
|
+
matchKind: candidate.matchKind,
|
|
56148
|
+
evidence: candidate.evidence
|
|
56149
|
+
}))
|
|
56150
|
+
};
|
|
56151
|
+
} else {
|
|
56152
|
+
const premiseWarning = `PREMISE CHECK: no component matching '${entityQuery}' exists in the vault \u2014 verify the name (a typo, or metadata newer than the last refresh; /sfi-refresh may help). The routed tool fails closed on an unknown component; do not present its error as an answer about a real component.`;
|
|
56153
|
+
route = {
|
|
56154
|
+
...route,
|
|
56155
|
+
confidence: "low",
|
|
56156
|
+
reason: `${route.reason} ${premiseWarning}`
|
|
56157
|
+
};
|
|
56158
|
+
entityEvidence = {
|
|
56159
|
+
query: entityQuery,
|
|
56160
|
+
typeHints: entityTypes,
|
|
56161
|
+
disposition: "none",
|
|
56162
|
+
clarificationRequired: false,
|
|
56163
|
+
warning: premiseWarning,
|
|
56164
|
+
candidates: []
|
|
56165
|
+
};
|
|
56166
|
+
}
|
|
56167
|
+
} else if (entityQuery !== null && refinedEntityResolution !== null && refinedEntityResolution.disposition === "ambiguous" && (/__(?:c|mdt|e|x|b|kav)\b/i.test(entityQuery) || entityQuery.includes(".")) && !refinedEntityResolution.candidates.some((candidate) => candidate.apiName.toLowerCase() === entityQuery.toLowerCase() || candidate.id.toLowerCase().endsWith(`:${entityQuery.toLowerCase()}`))) {
|
|
56168
|
+
const premiseWarning = `PREMISE CHECK: no component named '${entityQuery}' exists in the vault \u2014 the listed candidates are fuzzy lookalikes, not the component you named. Verify the name (a typo, or metadata newer than the last refresh; /sfi-refresh may help). The routed tool fails closed on an unknown component; do not present its error as an answer about a real component.`;
|
|
56169
|
+
route = {
|
|
56170
|
+
...route,
|
|
56171
|
+
confidence: "low",
|
|
56172
|
+
reason: `${route.reason} ${premiseWarning}`
|
|
56173
|
+
};
|
|
56174
|
+
if (entityEvidence !== void 0) {
|
|
56175
|
+
entityEvidence = { ...entityEvidence, warning: premiseWarning };
|
|
56176
|
+
}
|
|
56177
|
+
}
|
|
54953
56178
|
const clarificationId = clarificationIdFor(ctx.manifest.sourceTreeHash, route);
|
|
54954
56179
|
if (clarificationId !== null && route.clarification !== null) {
|
|
54955
56180
|
route = {
|
|
@@ -65549,7 +66774,7 @@ var init_tools = __esm({
|
|
|
65549
66774
|
similarityThreshold: { type: "number", minimum: 0, maximum: 1 },
|
|
65550
66775
|
includeTypeIncompatible: { type: "boolean" }
|
|
65551
66776
|
},
|
|
65552
|
-
required: ["
|
|
66777
|
+
required: ["objectA", "objectB"]
|
|
65553
66778
|
});
|
|
65554
66779
|
INTEGRATION_PROCEDURE_CHAIN_INPUT_SCHEMA = Object.freeze({
|
|
65555
66780
|
type: "object",
|
|
@@ -65661,7 +66886,7 @@ var init_tools = __esm({
|
|
|
65661
66886
|
},
|
|
65662
66887
|
{
|
|
65663
66888
|
name: "sfi.resolve",
|
|
65664
|
-
description: "Typo-tolerant resolver: messy/misspelled text -> ranked candidate components with a disposition (exact|ambiguous|none) + per-candidate evidence. Call FIRST when the user names a component informally; tolerates typos, filler, and the org's own misspellings that search_components cannot. CONFIRMED glossary annotations act as curated synonyms (candidates marked `glossary-alias`) \u2014 an alias never shadows an exact api-name match, and a synonym shared by two components yields `ambiguous` + clarification. Heuristic; never silently picks.",
|
|
66889
|
+
description: "Typo-tolerant resolver: messy/misspelled text -> ranked candidate components with a disposition (exact|ambiguous|none) + per-candidate evidence. Call FIRST when the user names a component informally; tolerates typos, filler, and the org's own misspellings that search_components cannot. Leading/trailing schema nouns are TYPE hints, not name content: 'SSN field' scores exactly like bare 'SSN' (the noun is stripped from fuzzy matching and instead prefers candidates of the hinted type among equally-confident matches \u2014 it never floats a weak fuzzy match of that type over an exact-name match of another). A dotted 'Object.Field' query is a definitive parent-scoped hit. CONFIRMED glossary annotations act as curated synonyms (candidates marked `glossary-alias`) \u2014 an alias never shadows an exact api-name match, and a synonym shared by two components yields `ambiguous` + clarification. Heuristic; never silently picks.",
|
|
65665
66890
|
inputSchema: RESOLVE_INPUT_SCHEMA
|
|
65666
66891
|
},
|
|
65667
66892
|
{
|
|
@@ -65691,7 +66916,7 @@ var init_tools = __esm({
|
|
|
65691
66916
|
},
|
|
65692
66917
|
{
|
|
65693
66918
|
name: "sfi.route_question",
|
|
65694
|
-
description: "Front-door router: for a plain-language question, surface a meaning-ranked shortlist of the sfi.* tools that can answer it \u2014 your host LLM picks which to run \u2014 plus the plane it belongs to (vault | live | hybrid | unknown), so the user never types a tool name. Read-only; it advises, it does not answer. Compound questions carry step ids and `dependsOn` edges: independent steps may run in parallel; a `then`-linked step waits for its prerequisite. On ambiguity it fails closed with `executionBlocked`, a clarification id, and offered options; resume deterministically by calling again with the exact same question plus `clarificationResponse: { clarificationId, selection }`. Stale ids and invented selections are rejected. Tells you when to sfi.resolve a named component first, whether the opt-in live plane is required, surfaces `suggestedArgs` (heuristic per-intent hints \u2014 e.g. `event: 'update'` for a save-order question so you can call `what_happens_on_save` without guessing the DML event), and \u2014 when the question hits a capability we lack \u2014 returns an honest 'unknown'/gap instead of fabricating (set `logGap: true` to also append the gap to the local backlog; off by default, privacy-first per CR-16); under `SFI_TOOL_PROFILE=core` the response also carries `invoke`: the routed tools as EXECUTABLE calls (core tools direct, everything else as the byte-identical `sfi.run_analysis` gateway envelope, suggestedArgs threaded) (a short phrase that merely NAMES a real vault component, with no question, is instead routed to sfi.resolve rather than 'unknown'). In the default hybrid mode the meaning-ranked `toolCandidates` are PRIMARY: every routable question carries the shortlist (offline TF-IDF over the capability map, no neural model, no network) plus a `guidance` line stating the loop YOU own \u2014 read the candidates \u2192 resolve any named component \u2192 pick/sequence the tool(s) \u2192 run them \u2192 ground via sfi.synthesize_answer. YOU decide which to run; the deterministic `route` rides along only as a non-authoritative HINT (suggested tool order + any resolved entity / suggestedArgs). Set `SFI_ROUTER_MODE=offline` for a deterministic, no-LLM route (Design A) where the route is authoritative and candidates are omitted \u2014 for CI / air-gapped hosts. An optional `mode` ('ask' | 'plan' | 'assessment') tailors the guidance and reranks the candidates toward that mode's family \u2014 'plan' favors the what_if_* / impact tools (an ordered change plan), 'assessment' favors the *_risk_report / readiness / coverage tools (a full evaluation), 'ask' is a quick grounded answer. Call this first on a vague/broad question to decide which tool(s) to run.",
|
|
66919
|
+
description: "Front-door router: for a plain-language question, surface a meaning-ranked shortlist of the sfi.* tools that can answer it \u2014 your host LLM picks which to run \u2014 plus the plane it belongs to (vault | live | hybrid | unknown), so the user never types a tool name. Read-only; it advises, it does not answer. Compound questions carry step ids and `dependsOn` edges: independent steps may run in parallel; a `then`-linked step waits for its prerequisite. On GENUINE ambiguity it fails closed with `executionBlocked`, a clarification id, and offered options; resume deterministically by calling again with the exact same question plus `clarificationResponse: { clarificationId, selection }`. Stale ids and invented selections are rejected. Bare schema nouns in the question (trigger/flow/field/object/profile/permission set/record type) are INTENT vocabulary, never named-entity lookups \u2014 'the Contact trigger' scopes a save-order question to the Contact OBJECT, it does not shop 'trigger' to the resolver; an object-qualified field ('Status__c on Case') is resolved parent-scoped; and an entity the resolver reports `exact` never raises the components-match menu \u2014 that menu is reserved for two genuinely competing components. The resolved TYPE also gates the routed tools: when the named entity resolves to a FLOW, tools that hard-error on a Flow id (call_graph / method_reachability / explain_apex_method, and the object/field access audits) are SWAPPED for the Flow-appropriate ones (who_can_run for access asks, explain_flow \u2014 which narrates the Apex the Flow invokes \u2014 plus get_impact) instead of routing into a guaranteed error. Qualifier words never outrank the HEAD question: 'bulk'/'load' on a save-order or test-coverage ask, 'seats'/'license' on a permission-set-assignment ask, and 'integration'/vendor words on a field-write / profile-security / what-if ask all stay on the head intent (never forced onto governor_limit_risks / live_license_usage / integration_map); field_lineage is reserved for explicit lineage/provenance questions ('where does this data come from'), never a field-adjacent default. FALSE PREMISE: when the question names a component the resolver cannot find \u2014 disposition `none`, or a literal API reference (dotted / __c-suffixed) that none of the fuzzy candidates actually match \u2014 the route is STILL returned (the routed tool fails closed on the unknown id) but its confidence is downgraded and `entityEvidence.warning` carries a premise disclosure (no component matching '<name>' exists in the vault \u2014 verify the name), so a nonexistent component is never answered as if it were real. Honesty routes: asks the surface genuinely cannot answer route to an explicit gap, never a lookalike tool \u2014 e.g. 'which USERS hold permission set X' (PermissionSetAssignment is not modeled; effective_permissions describes GRANTS, not holders); profile user-roster asks route live (live_group_count over User by ProfileId) with a partial-answer disclosure; and cross-vault compare routes carry a disclosure that a SECOND registered vault is required, so a single-vault install is never routed confident-clean into vault-not-found. Tells you when to sfi.resolve a named component first, whether the opt-in live plane is required, surfaces `suggestedArgs` (heuristic per-intent hints \u2014 e.g. `event: 'update'` for a save-order question so you can call `what_happens_on_save` without guessing the DML event), and \u2014 when the question hits a capability we lack \u2014 returns an honest 'unknown'/gap instead of fabricating (set `logGap: true` to also append the gap to the local backlog; off by default, privacy-first per CR-16); under `SFI_TOOL_PROFILE=core` the response also carries `invoke`: the routed tools as EXECUTABLE calls (core tools direct, everything else as the byte-identical `sfi.run_analysis` gateway envelope, suggestedArgs threaded) (a short phrase that merely NAMES a real vault component, with no question, is instead routed to sfi.resolve rather than 'unknown'). In the default hybrid mode the meaning-ranked `toolCandidates` are PRIMARY: every routable question carries the shortlist (offline TF-IDF over the capability map, no neural model, no network) plus a `guidance` line stating the loop YOU own \u2014 read the candidates \u2192 resolve any named component \u2192 pick/sequence the tool(s) \u2192 run them \u2192 ground via sfi.synthesize_answer. YOU decide which to run; the deterministic `route` rides along only as a non-authoritative HINT (suggested tool order + any resolved entity / suggestedArgs). Set `SFI_ROUTER_MODE=offline` for a deterministic, no-LLM route (Design A) where the route is authoritative and candidates are omitted \u2014 for CI / air-gapped hosts. An optional `mode` ('ask' | 'plan' | 'assessment') tailors the guidance and reranks the candidates toward that mode's family \u2014 'plan' favors the what_if_* / impact tools (an ordered change plan), 'assessment' favors the *_risk_report / readiness / coverage tools (a full evaluation), 'ask' is a quick grounded answer. Call this first on a vague/broad question to decide which tool(s) to run.",
|
|
65695
66920
|
inputSchema: ROUTE_QUESTION_INPUT_SCHEMA
|
|
65696
66921
|
},
|
|
65697
66922
|
{
|
|
@@ -65721,7 +66946,7 @@ var init_tools = __esm({
|
|
|
65721
66946
|
},
|
|
65722
66947
|
{
|
|
65723
66948
|
name: "sfi.list_components",
|
|
65724
|
-
description: 'List components of a given type (optionally narrowed by parentId), sorted by id. Paginated via limit/offset; `hasMore` hints at additional pages (a truncated page returns a `nextCursor` to resume). For `type: \'ApexClass\'`, optional boolean filters list interface/async/API implementers at the DB layer (correct pagination, not a post-filtered page): `isBatchable` / `isQueueable` / `isSchedulable` / `isRestResource` / `hasFutureMethod` / `hasInvocableMethod` / `hasAuraEnabledMethod` / `isTest` \u2014 e.g. `{ type: \'ApexClass\', isBatchable: true }` returns every Batchable class. When manifest coverage for the requested `type` is not `complete`, a structured `coverageCaveat` flags the inventory as potentially incomplete (scoped refresh, errored retrieve, not modeled) \u2014 including on non-empty pages. When the FIRST page is empty, a `retrievalHint` (FRESH-02) says WHY \u2014 "none in the org" (retrieved, none found) vs "not retrieved" (a scoped refresh skipped the type \u2014 run /sfi-refresh) vs "not modeled" \u2014 so an empty list is never a silent `[]` read as "the org has none". (The hint is suppressed when a boolean filter is active, since an empty filtered result is not a coverage gap.)',
|
|
66949
|
+
description: 'List components of a given type (optionally narrowed by parentId), sorted by id. Paginated via limit/offset; `hasMore` hints at additional pages (a truncated page returns a `nextCursor` to resume). Grant-heavy rows (Profile / PermissionSet, whose nodes carry tens of KB of declarative grants) are slimmed to scalar properties \u2014 each such row is marked `properties.propertiesTruncated: true` and the page carries a top-level `propertiesSlimmed: true` \u2014 so the whole inventory fits per page; fetch full detail per component via sfi.get_component. For `type: \'ApexClass\'`, optional boolean filters list interface/async/API implementers at the DB layer (correct pagination, not a post-filtered page): `isBatchable` / `isQueueable` / `isSchedulable` / `isRestResource` / `hasFutureMethod` / `hasInvocableMethod` / `hasAuraEnabledMethod` / `isTest` \u2014 e.g. `{ type: \'ApexClass\', isBatchable: true }` returns every Batchable class. When manifest coverage for the requested `type` is not `complete`, a structured `coverageCaveat` flags the inventory as potentially incomplete (scoped refresh, errored retrieve, not modeled) \u2014 including on non-empty pages. When the FIRST page is empty, a `retrievalHint` (FRESH-02) says WHY \u2014 "none in the org" (retrieved, none found) vs "not retrieved" (a scoped refresh skipped the type \u2014 run /sfi-refresh) vs "not modeled" \u2014 so an empty list is never a silent `[]` read as "the org has none". (The hint is suppressed when a boolean filter is active, since an empty filtered result is not a coverage gap.)',
|
|
65725
66950
|
inputSchema: LIST_COMPONENTS_INPUT_SCHEMA
|
|
65726
66951
|
},
|
|
65727
66952
|
{
|
|
@@ -65941,7 +67166,7 @@ var init_tools = __esm({
|
|
|
65941
67166
|
},
|
|
65942
67167
|
{
|
|
65943
67168
|
name: "sfi.effective_permissions",
|
|
65944
|
-
description: "Compute a user's EFFECTIVE access \u2014 the UNION of a profile + assigned permission sets, max-wins, with each permission attributed to the container(s) that grant it. `why_cant_user_see_record` evaluates a single record question against a bundle; nothing else rolls the containers up into one combined ability \u2014 this does. Input: `profileId` and/or `permissionSetIds[]` (at least one). A `PermissionSetGroup:` id may be passed in `permissionSetIds[]` \u2014 it is EXPANDED into its member permission sets (declared membership) and unioned in, so a PSG-assigned user gets a real answer (a permset reachable both directly and via a group is unioned once, not double-counted). It composes each container's outgoing `grantedBy` edges (object + field + apex)
|
|
67169
|
+
description: "Compute a user's EFFECTIVE access \u2014 the UNION of a profile + assigned permission sets, max-wins, with each permission attributed to the container(s) that grant it. `why_cant_user_see_record` evaluates a single record question against a bundle; nothing else rolls the containers up into one combined ability \u2014 this does. Input: `profileId` and/or `permissionSetIds[]` (at least one). A `PermissionSetGroup:` id may be passed in `permissionSetIds[]` \u2014 it is EXPANDED into its member permission sets (declared membership) and unioned in, so a PSG-assigned user gets a real answer (a permset reachable both directly and via a group is unioned once, not double-counted). It composes each container's outgoing `grantedBy` edges (object + field + apex), `properties.userPermissions` (system perms), and `properties.recordTypeVisibilities` (record-type visibility). `objectPermissions[]` carries the OR'd `allowCreate`/`allowRead`/`allowEdit`/`allowDelete`/`viewAllRecords`/`modifyAllRecords` per object plus `grantedBy` (the containers contributing a flag); `systemPermissions[]` lists each user-permission with its `grantedBy`; `customPermissions[]` (CR-CAP-10) lists each granted custom permission with its `grantedBy` + `targetMissing` (true when the granted name has no `CustomPermission` definition in the vault \u2014 managed-package / not-retrieved; declared but not resolvable, and NOT folded into systemPermissions); `recordTypeVisibilities[]` unions each container's declared record-type visibility (max-wins \u2014 visible=true wins; `<visible>` omitted in older metadata counts as visible, only an explicit false hides), each entry `{recordType, visible, grantedBy}` \u2014 record-type visibility is part of THIS union now, no longer only the separate `recordtype_availability` surface (that tool remains for the per-object grouped view); a container carrying no extracted `recordTypeVisibilities` property (a vault refreshed before record-type extraction) contributes nothing and is DISCLOSED (re-run /sfi-refresh), never fabricated as 'no record types'; `summary` reports objects / fieldsWithFls / apexClasses / systemPermissions / customPermissions / recordTypeVisibilities counts. The object list PAGES (`limit` default 100 / max 200, `offset`/`hasMore`/`truncated`). `declared` confidence. `disclosures` is explicit about the boundaries: permission-set GROUP membership IS expanded, but muting permission sets are DISCLOSED, not subtracted (effective access may be lower); app/tab visibility is a SEPARATE surface (now extracted \u2014 see `app_access` / `tab_availability`), not part of this union; field-level detail is summarised (use `field_access_audit`); object permission is NOT record access (record visibility needs OWD + sharing); custom permissions are declared grants, NOT system userPermissions, so they are never double-counted. Missing containers are ignored with a disclosure; if none exist \u2192 `component-not-found`.",
|
|
65945
67170
|
inputSchema: EFFECTIVE_PERMISSIONS_INPUT_SCHEMA
|
|
65946
67171
|
},
|
|
65947
67172
|
{
|
|
@@ -66480,7 +67705,7 @@ var init_tools = __esm({
|
|
|
66480
67705
|
},
|
|
66481
67706
|
{
|
|
66482
67707
|
name: "sfi.field_mapping_between_objects",
|
|
66483
|
-
description:
|
|
67708
|
+
description: "Map the fields of one object onto another for a conversion or migration \u2014 which field on object A corresponds to which field on object B, and which fields have no match and would lose data. Answers \"map {A} fields to {B} for conversion\", \"which {A} fields lose data converting to {B}\", \"what's the field mapping between these two objects\" (the classic Lead-to-Contact conversion mapping). v3.1 Q174 honesty-anchor tool: given TWO CustomObject api-names, returns a heuristic field pairing for migration mapping (the Lead-vs-Contact conversion case). `vault` is OPTIONAL \u2014 omit it (the normal single-vault install) to map within the SERVED vault; supply a registered alias only in a multi-vault-registry deployment. With no registry, a self-referential alias (the served vault's directory name or path) still answers from the served vault with a single-vault disclosure; any other alias gets an honest alias-not-found refusal plus the omit-`vault` hint. Each `FieldPair` carries `fieldA` / `fieldB` shape (apiName, label, type), the Jaccard `labelSimilarity` over tokenized api-name + label tokens, the `typeCompatible` flag from the static type-compatibility table (text\u2194text, number\u2194number, date\u2194date, picklist\u2194picklist, reference\u2194reference), and `confidence: 'heuristic'`. Optional `similarityThreshold` (default 0.50) suppresses pairs below the floor; `includeTypeIncompatible: true` retains label-matched pairs whose types disagree (each flagged `typeMismatch: true`). `unpairedFromA` / `unpairedFromB` list fields without a suggested match. `boundaries[]` ALWAYS surfaces the verbatim Q174 phrase: 'field-mapping suggestions are heuristic \u2014 labels are matched by token overlap and types by compatibility table. Verify each suggested pair against your business rules before relying on the mapping for a migration script.' A v3.1 release without this phrase is a contract violation regardless of test-suite green (PLAN-v3.1 \xA710 constitutional axis).",
|
|
66484
67709
|
inputSchema: FIELD_MAPPING_BETWEEN_OBJECTS_INPUT_SCHEMA
|
|
66485
67710
|
},
|
|
66486
67711
|
// v3.2 — OmniStudio composition tier. The
|
|
@@ -66550,7 +67775,7 @@ var init_tools = __esm({
|
|
|
66550
67775
|
},
|
|
66551
67776
|
{
|
|
66552
67777
|
name: "sfi.find_component_usages",
|
|
66553
|
-
description: 'The universal "where is this component used?" answer for ANY canonical component type (`componentId`) \u2014 one entry point instead of fanning out across find_field_anywhere / find_code_usages / get_impact / grep. Composes two evidence tiers: (1) GRAPH \u2014 incoming dependency edges to the target, grouped by referrer type, each carrying edge `confidence`, EXCLUDING access grants (`grantedBy`) and structural `parentOf` (access is not usage); (2) GREP supplement (`text-match` tier, `includeGrep` default true) \u2014 a literal search of Apex AND frontend bundle source (LWC/Aura/Visualforce \u2014 `$Label`/`$Resource`/`@salesforce` module references) for the api name, catching references the graph does not model (dynamic SOQL, reflective access, CustomMetadataType / CustomLabel / StaticResource refs). `graphReferrers[]` (type + count + sample), `grepSupplement` (matches with path/line/snippet), `summary` (counts + `hasStaticEvidence`), `boundaries[]`, `truncated`. HONESTY: empty graph + empty grep = "no static evidence in the vault" (in `boundaries`), NEVER "nothing uses this" \u2014 dynamic constructs, un-modeled families (reports/dashboards/list-views), and managed packages are invisible
|
|
67778
|
+
description: 'The universal "where is this component used?" answer for ANY canonical component type (`componentId`) \u2014 one entry point instead of fanning out across find_field_anywhere / find_code_usages / get_impact / grep. Composes two evidence tiers: (1) GRAPH \u2014 incoming dependency edges to the target, grouped by referrer type, each carrying edge `confidence`, EXCLUDING access grants (`grantedBy`) and structural `parentOf` (access is not usage); (2) GREP supplement (`text-match` tier, `includeGrep` default true) \u2014 a literal search of Apex AND frontend bundle source (LWC/Aura/Visualforce \u2014 `$Label`/`$Resource`/`@salesforce` module references) for the api name, catching references the graph does not model (dynamic SOQL, reflective access, CustomMetadataType / CustomLabel / StaticResource refs). `graphReferrers[]` (type + count + sample), `grepSupplement` (matches with path/line/snippet), `summary` (counts + `hasStaticEvidence`), `boundaries[]`, `truncated`. ACCESS-GRANT section: grants stay OUT of `graphReferrers`, but a separate `grantedBy` section (`{count, granters[{id,type}]}`) surfaces the granting containers when the target is a `CustomPermission` (its only incoming edges ARE grants \u2014 this answers "which Profiles / PermissionSets grant it?"; always present for that type, count 0 = no container grants it) or when the target has zero usage edges but incoming `grantedBy` edges; absent otherwise. HONESTY: empty graph + empty grep = "no static evidence in the vault" (in `boundaries`), NEVER "nothing uses this" \u2014 dynamic constructs, un-modeled families (reports/dashboards/list-views), and managed packages are invisible; grants are NOT usage, so `grantedBy` never counts toward `hasStaticEvidence`. Phantom-aware (a referenced-but-not-retrieved target still answers from its edges, including a granted-but-undefined managed-package CustomPermission). Specialized tools (find_field_anywhere, layout_assignments, \u2026) stay for a deeper single-family answer; this unifies the common case. Non-canonical id \u2192 `invalid-query`; an id with no node, no referrers AND no grants \u2192 `component-not-found`.',
|
|
66554
67779
|
inputSchema: FIND_COMPONENT_USAGES_INPUT_SCHEMA
|
|
66555
67780
|
},
|
|
66556
67781
|
{
|
|
@@ -68630,7 +69855,7 @@ var init_package_version = __esm({
|
|
|
68630
69855
|
"use strict";
|
|
68631
69856
|
readCliPackageVersion = () => {
|
|
68632
69857
|
if (true)
|
|
68633
|
-
return "0.1.
|
|
69858
|
+
return "0.1.21";
|
|
68634
69859
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
68635
69860
|
try {
|
|
68636
69861
|
const raw = readFileSync3(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
|
@@ -68647,7 +69872,7 @@ var init_package_version = __esm({
|
|
|
68647
69872
|
|
|
68648
69873
|
// dist/src/refresh-pipeline.js
|
|
68649
69874
|
import { mkdir as mkdir10, readdir as readdir5, stat as stat7, writeFile as writeFile9 } from "node:fs/promises";
|
|
68650
|
-
import { basename as
|
|
69875
|
+
import { basename as basename14, dirname as dirname20, join as join28, sep as sep3 } from "node:path";
|
|
68651
69876
|
var EXTRACT_CACHE_VERSION, SUPPORTED_TYPES, EXTRACTORS, dispatchFile, BUNDLE_PARENT_DIRS, walkDir, relativeSegments, DX_WRAPPER_SEGMENTS, KNOWN_SIDECAR_SUFFIXES, isKnownSidecar, skipAttributionKey, componentTypeFromSourcePath, FOLD_TO_FIELD_USAGE, foldReportDashboardUsageIntoFields, walkAndExtract, composeDocument, parentApiNameFor, writeNodeDocument, writeIndex, RENDER_PAGE_SIZE, renderVault, parseTypeFilter;
|
|
68652
69877
|
var init_refresh_pipeline = __esm({
|
|
68653
69878
|
"dist/src/refresh-pipeline.js"() {
|
|
@@ -68954,7 +70179,7 @@ var init_refresh_pipeline = __esm({
|
|
|
68954
70179
|
return;
|
|
68955
70180
|
}
|
|
68956
70181
|
const sorted = entries.filter((entry) => !entry.name.startsWith(".")).sort((a2, b2) => a2.name < b2.name ? -1 : a2.name > b2.name ? 1 : 0);
|
|
68957
|
-
const currentName =
|
|
70182
|
+
const currentName = basename14(currentDir);
|
|
68958
70183
|
const isBundleParent = BUNDLE_PARENT_DIRS.has(currentName);
|
|
68959
70184
|
for (const entry of sorted) {
|
|
68960
70185
|
const abs = join28(currentDir, entry.name);
|
|
@@ -69005,7 +70230,7 @@ var init_refresh_pipeline = __esm({
|
|
|
69005
70230
|
};
|
|
69006
70231
|
componentTypeFromSourcePath = (sourceRoot, absPath, isDirectory = false) => {
|
|
69007
70232
|
const segments = relativeSegments(sourceRoot, absPath);
|
|
69008
|
-
const fileName = isDirectory ?
|
|
70233
|
+
const fileName = isDirectory ? basename14(absPath) : segments[segments.length - 1] ?? basename14(absPath);
|
|
69009
70234
|
const dirSegments = isDirectory ? segments : segments.slice(0, -1);
|
|
69010
70235
|
return dispatchFile(dirSegments, fileName, isDirectory);
|
|
69011
70236
|
};
|
|
@@ -69203,7 +70428,7 @@ ${body}
|
|
|
69203
70428
|
|
|
69204
70429
|
// dist/src/source-reconcile.js
|
|
69205
70430
|
import { cp, readdir as readdir6, rm as rm3, stat as stat8 } from "node:fs/promises";
|
|
69206
|
-
import { basename as
|
|
70431
|
+
import { basename as basename15, join as join29, relative as relative3, sep as sep4 } from "node:path";
|
|
69207
70432
|
var BUNDLE_PARENT_DIRS2, KNOWN_SIDECAR_SUFFIXES2, isKnownSidecar2, primaryRelPathForSidecar, walkSourceEntries, authoritativePathSet, reconcileSourceDeletions, syncAuthoritativeRetrieveIntoSource;
|
|
69208
70433
|
var init_source_reconcile = __esm({
|
|
69209
70434
|
"dist/src/source-reconcile.js"() {
|
|
@@ -69239,7 +70464,7 @@ var init_source_reconcile = __esm({
|
|
|
69239
70464
|
return;
|
|
69240
70465
|
}
|
|
69241
70466
|
const sorted = entries.filter((entry) => !entry.name.startsWith(".")).sort((a2, b2) => a2.name < b2.name ? -1 : a2.name > b2.name ? 1 : 0);
|
|
69242
|
-
const isBundleParent = BUNDLE_PARENT_DIRS2.has(
|
|
70467
|
+
const isBundleParent = BUNDLE_PARENT_DIRS2.has(basename15(currentDir));
|
|
69243
70468
|
for (const entry of sorted) {
|
|
69244
70469
|
const abs = join29(currentDir, entry.name);
|
|
69245
70470
|
if (entry.isDirectory()) {
|
|
@@ -69296,7 +70521,7 @@ var init_source_reconcile = __esm({
|
|
|
69296
70521
|
for (const entry of sourceEntries) {
|
|
69297
70522
|
if (!typesToReconcile.has(entry.type))
|
|
69298
70523
|
continue;
|
|
69299
|
-
const fileName =
|
|
70524
|
+
const fileName = basename15(entry.absPath);
|
|
69300
70525
|
const primaryRel = primaryRelPathForSidecar(entry.relPath, fileName);
|
|
69301
70526
|
const authoritativeKey = primaryRel ?? entry.relPath;
|
|
69302
70527
|
if (authoritative.has(authoritativeKey) || authoritative.has(entry.relPath))
|
|
@@ -138554,7 +139779,7 @@ var makeShutdownOnce = (ctx) => {
|
|
|
138554
139779
|
|
|
138555
139780
|
// dist/src/commands/demo.js
|
|
138556
139781
|
init_refresh();
|
|
138557
|
-
var buildVersion = () => true ? "0.1.
|
|
139782
|
+
var buildVersion = () => true ? "0.1.21" : "dev";
|
|
138558
139783
|
var SHUTDOWN_SIGNALS2 = ["SIGINT", "SIGTERM"];
|
|
138559
139784
|
var resolveDemoSource = () => {
|
|
138560
139785
|
let dir = dirname21(fileURLToPath2(import.meta.url));
|
|
@@ -139539,7 +140764,7 @@ init_vault_git();
|
|
|
139539
140764
|
init_watch();
|
|
139540
140765
|
var readVersion = () => {
|
|
139541
140766
|
if (true)
|
|
139542
|
-
return "0.1.
|
|
140767
|
+
return "0.1.21";
|
|
139543
140768
|
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
139544
140769
|
const raw = readFileSync6(fileURLToPath3(pkgUrl), "utf8");
|
|
139545
140770
|
const parsed = JSON.parse(raw);
|