UNPKG

powerplatform-review-tool

Version:

Evaluate Power Platform solution zip files based on best practice patterns

580 lines (579 loc) 28.1 kB
"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, }; }