powerplatform-review-tool
Version:
Evaluate Power Platform solution zip files based on best practice patterns
580 lines (579 loc) • 28.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.patternCheckTriggerPhraseCount = patternCheckTriggerPhraseCount;
exports.patternCheckSingleWordPhrases = patternCheckSingleWordPhrases;
exports.patternCheckLongPhrases = patternCheckLongPhrases;
exports.patternCheckConditionCount = patternCheckConditionCount;
exports.patternCheckFlowCount = patternCheckFlowCount;
exports.patternCheckSharePointAuthMode = patternCheckSharePointAuthMode;
exports.patternCheckUnusedTopics = patternCheckUnusedTopics;
exports.patternCheckSynonymsQuality = patternCheckSynonymsQuality;
exports.patternCheckDuplicateRegexEntities = patternCheckDuplicateRegexEntities;
exports.patternCheckEndOfConversationUsage = patternCheckEndOfConversationUsage;
/* eslint-disable @typescript-eslint/no-explicit-any */
const ManifestConstant_1 = require("../ManifestConstant");
const bothelper_1 = require("../utilities/bothelper");
/**
* This function checks if each topic contains at least 5 trigger phrases.
* It identifies topics that do not meet this requirement and returns a result indicating whether
* all topics pass the check.
*
* @param topicsArray - A record of topics and their associated trigger queries.
* @returns An object containing the pattern name, status, instance value, failure reason, and recommendation.
*/
/**
* Now receives an array of topic objects. We extract the trigger phrases for each
* topic in the function, and check if less than 5.
*/
function patternCheckTriggerPhraseCount(topicsArray) {
return new Promise((resolve) => {
const invalidTopics = [];
topicsArray.forEach((t) => {
// skip if topic is not enabled
if (!t.isEnabled) {
return;
}
const triggerPhrases = (0, bothelper_1.extractTriggerPhrases)(t.data);
if (triggerPhrases.length < 5) {
invalidTopics.push({
TopicName: t.name,
});
}
});
const isPassed = invalidTopics.length === 0;
resolve({
patternID: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.patternID,
patternName: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.patternName,
description: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: [
{
Topic: !isPassed ? invalidTopics : undefined,
Flows: undefined,
KnowledgeSource: undefined,
},
],
failureReason: !isPassed ? "Some topics have less than 5 trigger queries." : "",
recommendation: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.docLink,
severity: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.severity,
category: ManifestConstant_1.BotPatternDetails.TriggerPhraseCount.category,
});
});
}
/**
* This function checks if any of the trigger phrases in the topics are single words.
* It identifies topics that contain single-word phrases and returns a result.
*
* @param topicsArray - A record of topics and their associated trigger queries.
* @returns An object containing the pattern name, status, instance value, failure reason, and recommendation.
*/
function patternCheckSingleWordPhrases(topicsArray) {
return new Promise((resolve) => {
const agentTopics = [];
topicsArray.forEach((t) => {
// skip if topic is disabled
if (!t.isEnabled) {
return;
}
const triggers = (0, bothelper_1.extractTriggerPhrases)(t.data);
const invalidPhrases = triggers.filter((phrase) => phrase.trim().split(/\s+/).length === 1);
if (invalidPhrases.length > 0) {
agentTopics.push({
TopicName: t.name,
InvalidTriggerQueries: invalidPhrases,
});
}
});
const isPassed = agentTopics.length === 0;
resolve({
patternID: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.patternID,
patternName: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.patternName,
description: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: [
{
Topic: !isPassed ? agentTopics : undefined,
Flows: undefined,
KnowledgeSource: undefined,
},
],
failureReason: !isPassed ? "Some trigger queries are single words." : "",
recommendation: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.docLink,
severity: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.severity,
category: ManifestConstant_1.BotPatternDetails.SingleWordPhrases.category,
});
});
}
/**
* This function checks if any of the trigger phrases in the topics exceed 10 words in length.
* It identifies topics with overly long phrases and returns a result.
*
* @param topicsArray - An array of topics.
* @returns An object containing the pattern name, status, instance value, failure reason, and recommendation.
*/
function patternCheckLongPhrases(topicsArray) {
return new Promise((resolve) => {
const agentTopics = [];
topicsArray.forEach((t) => {
// skip if topic is disabled
if (!t.isEnabled) {
return;
}
const triggers = (0, bothelper_1.extractTriggerPhrases)(t.data);
const invalidPhrases = triggers.filter((phrase) => phrase.trim().split(/\s+/).length > 10);
if (invalidPhrases.length > 0) {
agentTopics.push({
TopicName: t.name,
InvalidTriggerQueries: invalidPhrases,
});
}
});
const isPassed = agentTopics.length === 0;
resolve({
patternID: ManifestConstant_1.BotPatternDetails.LongPhrases.patternID,
patternName: ManifestConstant_1.BotPatternDetails.LongPhrases.patternName,
description: ManifestConstant_1.BotPatternDetails.LongPhrases.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: [
{
Topic: !isPassed ? agentTopics : undefined,
Flows: undefined,
KnowledgeSource: undefined,
},
],
failureReason: !isPassed ? "Some trigger queries are too long." : "",
recommendation: ManifestConstant_1.BotPatternDetails.LongPhrases.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.LongPhrases.docLink,
severity: ManifestConstant_1.BotPatternDetails.LongPhrases.severity,
category: ManifestConstant_1.BotPatternDetails.LongPhrases.category,
});
});
}
async function patternCheckConditionCount(topics) {
const topicExceedingConditions = [];
topics.forEach((topic) => {
// skip if disabled
if (!topic.isEnabled) {
return;
}
const topicData = topic.data;
if (topicData && topicData.beginDialog && topicData.beginDialog.actions) {
const totalConditions = (0, bothelper_1.countConditionsInActions)(topicData.beginDialog.actions);
if (totalConditions > 15) {
topicExceedingConditions.push({
TopicName: topic.name,
});
}
}
});
const isPassed = topicExceedingConditions.length === 0;
return {
patternID: ManifestConstant_1.BotPatternDetails.ConditionCount.patternID,
patternName: ManifestConstant_1.BotPatternDetails.ConditionCount.patternName,
description: ManifestConstant_1.BotPatternDetails.ConditionCount.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: [
{
Topic: !isPassed ? topicExceedingConditions : undefined,
Flows: undefined,
KnowledgeSource: undefined,
},
],
failureReason: !isPassed ? "Some topic(s) have more than 15 conditions." : "",
recommendation: ManifestConstant_1.BotPatternDetails.ConditionCount.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.ConditionCount.docLink,
severity: ManifestConstant_1.BotPatternDetails.ConditionCount.severity,
category: ManifestConstant_1.BotPatternDetails.ConditionCount.category,
};
}
async function patternCheckFlowCount(topics) {
const uniqueFlowIds = new Set();
topics.forEach((topic) => {
// skip if topic is disabled
if (!topic.isEnabled) {
return;
}
const topicData = topic.data;
if (topicData && topicData.beginDialog && topicData.beginDialog.actions) {
(0, bothelper_1.extractUniqueFlowIds)(topicData.beginDialog.actions, uniqueFlowIds);
}
});
const uniqueFlowIdCount = uniqueFlowIds.size;
const isPatternFailed = uniqueFlowIdCount > 10;
const flowID = isPatternFailed ? Array.from(uniqueFlowIds) : [];
return {
patternID: ManifestConstant_1.BotPatternDetails.FlowCount.patternID,
patternName: ManifestConstant_1.BotPatternDetails.FlowCount.patternName,
description: ManifestConstant_1.BotPatternDetails.FlowCount.description,
status: !isPatternFailed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, // Pass if count ≤ 10, fail if > 10
instanceValue: [
{
Topic: undefined,
Flows: flowID,
KnowledgeSource: undefined,
},
],
failureReason: isPatternFailed ? `More than 10 Flows found (${uniqueFlowIdCount}).` : "",
recommendation: ManifestConstant_1.BotPatternDetails.FlowCount.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.FlowCount.docLink,
severity: ManifestConstant_1.BotPatternDetails.FlowCount.severity,
category: ManifestConstant_1.BotPatternDetails.FlowCount.category,
};
}
async function patternCheckSharePointAuthMode(finalBotData) {
// Retrieve authentication mode as a string: "1", "2", or "3"
const authModeNum = (await (0, bothelper_1.getAuthenticationMode)(finalBotData.botXml)).toString();
// Map numeric auth mode to textual auth mode in AgentAuthentication
const mappedAuthMode = (0, bothelper_1.mapAuthMode)(authModeNum);
// Check for knowledge components with SharePointSearchSource
// and authentication mode not equal to "Manual" (i.e. authModeNum !== "3")
const knowledgeArray = finalBotData.botComponents.knowledge || [];
// Filter for any SharePointSearchSource items with the wrong auth mode
const sharePointKnowledgeSources = knowledgeArray.filter((component) => component.data?.source?.kind === "SharePointSearchSource" && authModeNum === "1");
const isPatternFailed = sharePointKnowledgeSources.length > 0;
return {
patternID: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.patternID,
patternName: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.patternName,
description: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.description,
status: !isPatternFailed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, // Pass if no invalid SharePoint sources found
instanceValue: isPatternFailed
? [
{
Topic: undefined,
Flows: undefined,
Entity: undefined,
KnowledgeSource: sharePointKnowledgeSources.map((comp) => comp.name),
},
]
: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: isPatternFailed
? `Found SharePoint knowledge sources with incorrect authentication mode '${mappedAuthMode}'.`
: "",
recommendation: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.docLink,
severity: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.severity,
category: ManifestConstant_1.BotPatternDetails.SharePointAuthMode.category,
};
}
/**
* Checks for topics that have no trigger queries and are not referenced by any other topic
* in a `BeginDialog` action's `dialog` property.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
async function patternCheckUnusedTopics(topics, botLogicalName) {
const usedTopicRefs = new Set();
const enabledTopics = topics.filter((t) => t.isEnabled);
// 1. Gather references from each topic's actions
for (const topic of enabledTopics) {
if (topic.data?.beginDialog?.actions) {
(0, bothelper_1.collectBeginDialogReferences)(topic.data.beginDialog.actions, usedTopicRefs);
}
}
// 2. Identify topics without trigger queries & not referenced anywhere
const unusedTopics = [];
for (const topic of enabledTopics) {
const triggerQueries = topic.data?.beginDialog?.intent?.triggerQueries || [];
if (triggerQueries.length === 0) {
// Format the reference "botLogicalName.topic.TopicNameWithoutSpaces"
const topicRef = (0, bothelper_1.formatTopicName)(botLogicalName, topic.name);
if (!usedTopicRefs.has(topicRef)) {
unusedTopics.push(topic.name);
}
}
}
const hasFailures = unusedTopics.length > 0;
return {
patternID: ManifestConstant_1.BotPatternDetails.UnusedTopics.patternID,
patternName: ManifestConstant_1.BotPatternDetails.UnusedTopics.patternName,
description: ManifestConstant_1.BotPatternDetails.UnusedTopics.description,
status: !hasFailures ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */, // fails if there are any unused topics
instanceValue: hasFailures
? [
{
Topic: unusedTopics.map((t) => ({
TopicName: t,
InvalidTriggerQueries: undefined,
})),
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
]
: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: hasFailures
? "Some topics do not have trigger queries and are not referenced in any other topics."
: "",
recommendation: ManifestConstant_1.BotPatternDetails.UnusedTopics.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.UnusedTopics.docLink,
severity: ManifestConstant_1.BotPatternDetails.UnusedTopics.severity,
category: ManifestConstant_1.BotPatternDetails.UnusedTopics.category,
};
}
/**
* Checks synonyms within ClosedListEntity definitions to ensure no synonym is more than 2 words long.
* Returns an array of objects, each containing just { entityName, longSynonyms }.
*
* If `entityArray` is null, undefined, or empty, it automatically **passes**.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
async function patternCheckSynonymsQuality(entityArray) {
// 1. Auto-pass if no array or empty
if (!entityArray || entityArray.length === 0) {
return {
patternID: ManifestConstant_1.BotPatternDetails.SynonymsQuality.patternID,
patternName: ManifestConstant_1.BotPatternDetails.SynonymsQuality.patternName,
description: ManifestConstant_1.BotPatternDetails.SynonymsQuality.description,
status: "Pass" /* ManifestPropertyStatusNames.Pass */, // auto-pass if no entities
instanceValue: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: "",
recommendation: ManifestConstant_1.BotPatternDetails.SynonymsQuality.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.SynonymsQuality.docLink,
severity: ManifestConstant_1.BotPatternDetails.SynonymsQuality.severity,
category: ManifestConstant_1.BotPatternDetails.SynonymsQuality.category,
};
}
// 2. Collect any synonyms that exceed 2 words
const closedListFailures = [];
for (const entity of entityArray) {
if (entity.data?.kind === "ClosedListEntity" && Array.isArray(entity.data.items)) {
const entityLongSynonyms = new Set();
for (const item of entity.data.items) {
const synonyms = Array.isArray(item.synonyms) ? item.synonyms : [];
for (const synonym of synonyms) {
const wordCount = synonym.trim().split(/\s+/).length;
if (wordCount > 2) {
entityLongSynonyms.add(synonym);
}
}
}
if (entityLongSynonyms.size > 0) {
closedListFailures.push({
name: entity.name,
longSynonyms: Array.from(entityLongSynonyms),
});
}
}
}
const isPassed = closedListFailures.length === 0;
// 3. Return a PatternResult with an AgentInstanceValue
return {
patternID: ManifestConstant_1.BotPatternDetails.SynonymsQuality.patternID,
patternName: ManifestConstant_1.BotPatternDetails.SynonymsQuality.patternName,
description: ManifestConstant_1.BotPatternDetails.SynonymsQuality.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: isPassed
? [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
]
: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: {
ClosedListEntity: closedListFailures.map((failure) => ({
name: failure.name,
longSynonyms: failure.longSynonyms,
})),
},
},
],
failureReason: !isPassed ? "Some ClosedListEntity synonyms exceed 2 words." : "",
recommendation: ManifestConstant_1.BotPatternDetails.SynonymsQuality.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.SynonymsQuality.docLink,
severity: ManifestConstant_1.BotPatternDetails.SynonymsQuality.severity,
category: ManifestConstant_1.BotPatternDetails.SynonymsQuality.category,
};
}
/**
* Checks for duplicate RegexEntity patterns across entities in finalBotData.botComponents.entity.
* If multiple entities use the same `pattern` property, it is flagged as a failure.
*
* If `entityArray` is null, undefined, or empty, it automatically **passes**.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
async function patternCheckDuplicateRegexEntities(entityArray) {
// 1. Auto-pass if no array or empty
if (!entityArray || entityArray.length === 0) {
return {
patternID: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.patternID,
patternName: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.patternName,
description: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.description,
status: "Pass" /* ManifestPropertyStatusNames.Pass */,
instanceValue: [
{
// Explicitly set all properties to undefined
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: "",
recommendation: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.docLink,
severity: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.severity,
category: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.category,
};
}
// 2. Build a map from pattern -> array of entity names
const patternMap = new Map();
for (const entity of entityArray) {
if (entity.data?.kind === "RegexEntity" && entity.data?.pattern) {
const pattern = entity.data.pattern;
if (!patternMap.has(pattern)) {
patternMap.set(pattern, []);
}
patternMap.get(pattern)?.push(entity.name);
}
}
// 3. Identify duplicates (just 1 object per pattern group)
const duplicateResults = [];
for (const [regexPattern, entityNames] of patternMap.entries()) {
if (entityNames.length > 1) {
// We create exactly one record for all entities sharing this pattern
duplicateResults.push({
// Use the first entity as "name"
name: entityNames[0],
pattern: regexPattern,
// The rest go into matchingPatternEntityList
matchingPatternEntityList: entityNames.slice(1),
});
}
}
const isPassed = duplicateResults.length === 0;
return {
patternID: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.patternID,
patternName: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.patternName,
description: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: isPassed
? [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
]
: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: {
RegexEntity: duplicateResults,
},
},
],
failureReason: !isPassed ? "Multiple entities of kind RegexEntity use the same regex pattern." : "",
recommendation: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.docLink,
severity: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.severity,
category: ManifestConstant_1.BotPatternDetails.DuplicateRegexEntities.category,
};
}
/**
* Checks if there's at least one End Of Conversation topic (enabled) and at least one other topic (enabled)
* that references it.
*/
async function patternCheckEndOfConversationUsage(topics, botLogicalName) {
// Filter only by isEnabled
const eocSearchTopics = topics.filter((t) => t.isEnabled);
// 1) Collect all EndOfConversation topics from eocSearchTopics
const endTopics = [];
for (const t of eocSearchTopics) {
if ((0, bothelper_1.isEndOfConversationTopic)(t.data)) {
endTopics.push(t.name);
}
}
// If none found, fail
if (endTopics.length === 0) {
return {
patternID: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.patternID,
patternName: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.patternName,
description: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.description,
status: "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: "No End Of Conversation topic found.",
recommendation: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.docLink,
severity: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.severity,
category: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.category,
};
}
// Collect references only from enabled topics
const references = new Set();
for (const t of eocSearchTopics) {
if (t.data?.beginDialog?.actions) {
(0, bothelper_1.collectBeginDialogReferences)(t.data.beginDialog.actions, references);
}
}
// Format end topic references: "botLogicalName.topic.TopicNameNoSpaces"
const endTopicRefs = endTopics.map((et) => (0, bothelper_1.formatTopicName)(botLogicalName, et));
const foundReference = endTopicRefs.some((etr) => references.has(etr));
const isPassed = foundReference;
return {
patternID: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.patternID,
patternName: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.patternName,
description: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.description,
status: isPassed ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */,
instanceValue: isPassed
? [
{
Topic: undefined,
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
]
: [
{
Topic: endTopics.map((et) => ({ TopicName: et })),
Flows: undefined,
KnowledgeSource: undefined,
Entity: undefined,
},
],
failureReason: !isPassed ? "None of the enabled topics redirect to any End Of Conversation topic." : "",
recommendation: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.recommendation,
docLinks: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.docLink,
severity: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.severity,
category: ManifestConstant_1.BotPatternDetails.EndOfConversationReference.category,
};
}