mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-05-11 03:00:42 -04:00
Refactor results to make check complexity easier to visualize in log
* Differentiate rule results from rule set results * Implement function to parse all results for check and print and/or PLUS triggered state for all rule/sets in check
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import {RuleSet, IRuleSet, RuleSetJson, RuleSetObjectJson} from "../Rule/RuleSet";
|
||||
import {IRule, Rule, RuleJSONConfig, RuleResult} from "../Rule";
|
||||
import {IRule, isRuleSetResult, Rule, RuleJSONConfig, RuleResult, RuleSetResult} from "../Rule";
|
||||
import Action, {ActionConfig, ActionJson} from "../Action";
|
||||
import {Logger} from "winston";
|
||||
import {Comment, Submission} from "snoowrap";
|
||||
import {actionFactory} from "../Action/ActionFactory";
|
||||
import {ruleFactory} from "../Rule/RuleFactory";
|
||||
import {createAjvFactory, mergeArr, ruleNamesFromResults} from "../util";
|
||||
import {createAjvFactory, mergeArr, resultsSummary, ruleNamesFromResults} from "../util";
|
||||
import {
|
||||
ChecksActivityState,
|
||||
CommentState,
|
||||
@@ -147,11 +147,12 @@ export class Check implements ICheck {
|
||||
}
|
||||
|
||||
async runRules(item: Submission | Comment, existingResults: RuleResult[] = []): Promise<[boolean, RuleResult[]]> {
|
||||
let allResults: RuleResult[] = [];
|
||||
let allRuleResults: RuleResult[] = [];
|
||||
let allResults: (RuleResult | RuleSetResult)[] = [];
|
||||
const [itemPass, crit] = isItem(item, this.itemIs, this.logger);
|
||||
if(!itemPass) {
|
||||
this.logger.verbose(`❌ => Item did not pass 'itemIs' test`);
|
||||
return [false, allResults];
|
||||
return [false, allRuleResults];
|
||||
}
|
||||
let authorPass = null;
|
||||
if (this.authorIs.include !== undefined && this.authorIs.include.length > 0) {
|
||||
@@ -163,7 +164,7 @@ export class Check implements ICheck {
|
||||
}
|
||||
if(!authorPass) {
|
||||
this.logger.verbose('❌ => Inclusive author criteria not matched');
|
||||
return Promise.resolve([false, allResults]);
|
||||
return Promise.resolve([false, allRuleResults]);
|
||||
}
|
||||
}
|
||||
if (authorPass === null && this.authorIs.exclude !== undefined && this.authorIs.exclude.length > 0) {
|
||||
@@ -175,45 +176,52 @@ export class Check implements ICheck {
|
||||
}
|
||||
if(!authorPass) {
|
||||
this.logger.verbose('❌ => Exclusive author criteria not matched');
|
||||
return Promise.resolve([false, allResults]);
|
||||
return Promise.resolve([false, allRuleResults]);
|
||||
}
|
||||
}
|
||||
|
||||
if(this.rules.length === 0) {
|
||||
this.logger.info(`✔️ => No rules to run, check auto-passes`);
|
||||
return [true, allResults];
|
||||
return [true, allRuleResults];
|
||||
}
|
||||
|
||||
let runOne = false;
|
||||
for (const r of this.rules) {
|
||||
const combinedResults = [...existingResults, ...allResults];
|
||||
//let results: RuleResult | RuleSetResult;
|
||||
const combinedResults = [...existingResults, ...allRuleResults];
|
||||
const [passed, results] = await r.run(item, combinedResults);
|
||||
allResults = allResults.concat(results);
|
||||
if(isRuleSetResult(results)) {
|
||||
allRuleResults = allRuleResults.concat(results.results);
|
||||
} else {
|
||||
allRuleResults = allRuleResults.concat(results as RuleResult);
|
||||
}
|
||||
allResults.push(results);
|
||||
if (passed === null) {
|
||||
continue;
|
||||
}
|
||||
debugger;
|
||||
runOne = true;
|
||||
if (passed) {
|
||||
if (this.condition === 'OR') {
|
||||
this.logger.info(`✔️ => Rules (OR): ${ruleNamesFromResults(allResults)}`);
|
||||
return [true, allResults];
|
||||
this.logger.info(`✔️ => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [true, allRuleResults];
|
||||
}
|
||||
} else if (this.condition === 'AND') {
|
||||
this.logger.info(`❌ => Rules (AND): ${ruleNamesFromResults(allResults)}`);
|
||||
return [false, allResults];
|
||||
this.logger.info(`❌ => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [false, allRuleResults];
|
||||
}
|
||||
}
|
||||
if (!runOne) {
|
||||
this.logger.verbose('❌ => All Rules skipped because of Author checks or itemIs tests');
|
||||
return [false, allResults];
|
||||
return [false, allRuleResults];
|
||||
} else if(this.condition === 'OR') {
|
||||
// if OR and did not return already then none passed
|
||||
this.logger.verbose(`❌ => Rules (OR): ${ruleNamesFromResults(allResults)}`);
|
||||
return [false, allResults];
|
||||
this.logger.verbose(`❌ => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [false, allRuleResults];
|
||||
}
|
||||
// otherwise AND and did not return already so all passed
|
||||
this.logger.info(`✔️ => Rules (AND) : ${ruleNamesFromResults(allResults)}`);
|
||||
return [true, allResults];
|
||||
this.logger.info(`✔️ => Rules: ${resultsSummary(allResults, this.condition)}`);
|
||||
return [true, allRuleResults];
|
||||
}
|
||||
|
||||
async runActions(item: Submission | Comment, ruleResults: RuleResult[]): Promise<Action[]> {
|
||||
|
||||
@@ -53,21 +53,21 @@ export class AuthorRule extends Rule {
|
||||
};
|
||||
}
|
||||
|
||||
protected async process(item: Comment | Submission): Promise<[boolean, RuleResult[]]> {
|
||||
protected async process(item: Comment | Submission): Promise<[boolean, RuleResult]> {
|
||||
if (this.include.length > 0) {
|
||||
for (const auth of this.include) {
|
||||
if (await this.resources.testAuthorCriteria(item, auth)) {
|
||||
return Promise.resolve([true, [this.getResult(true)]]);
|
||||
return Promise.resolve([true, this.getResult(true)]);
|
||||
}
|
||||
}
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
for (const auth of this.exclude) {
|
||||
if (await this.resources.testAuthorCriteria(item, auth, false)) {
|
||||
return Promise.resolve([true, [this.getResult(true)]]);
|
||||
return Promise.resolve([true, this.getResult(true)]);
|
||||
}
|
||||
}
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export class HistoryRule extends Rule {
|
||||
}
|
||||
}
|
||||
|
||||
protected async process(item: Submission): Promise<[boolean, RuleResult[]]> {
|
||||
protected async process(item: Submission): Promise<[boolean, RuleResult]> {
|
||||
// TODO reuse activities between ActivityCriteria to reduce api calls
|
||||
|
||||
let criteriaResults = [];
|
||||
@@ -239,15 +239,15 @@ export class HistoryRule extends Rule {
|
||||
|
||||
const result = `${thresholdSummary} (${data.window})`;
|
||||
this.logger.verbose(result);
|
||||
return Promise.resolve([true, [this.getResult(true, {
|
||||
return Promise.resolve([true, this.getResult(true, {
|
||||
result,
|
||||
data,
|
||||
})]]);
|
||||
})]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class RecentActivityRule extends Rule {
|
||||
}
|
||||
}
|
||||
|
||||
async process(item: Submission | Comment): Promise<[boolean, RuleResult[]]> {
|
||||
async process(item: Submission | Comment): Promise<[boolean, RuleResult]> {
|
||||
let activities;
|
||||
|
||||
switch (this.lookAt) {
|
||||
@@ -135,7 +135,7 @@ export class RecentActivityRule extends Rule {
|
||||
}
|
||||
const result = resultArr.join(' and ')
|
||||
this.logger.verbose(result);
|
||||
return Promise.resolve([true, [this.getResult(true, {
|
||||
return Promise.resolve([true, this.getResult(true, {
|
||||
result,
|
||||
data: {
|
||||
window: typeof this.window === 'number' ? `${activities.length} Items` : activityWindowText(viableActivity),
|
||||
@@ -145,10 +145,10 @@ export class RecentActivityRule extends Rule {
|
||||
subCount: data.totalSubredditsCount,
|
||||
totalCount: data.totalCount
|
||||
}
|
||||
})]]);
|
||||
})]);
|
||||
}
|
||||
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {IRule, Triggerable, Rule, RuleJSONConfig, RuleResult} from "./index";
|
||||
import {IRule, Triggerable, Rule, RuleJSONConfig, RuleResult, RuleSetResult} from "./index";
|
||||
import {Comment, Submission} from "snoowrap";
|
||||
import {ruleFactory} from "./RuleFactory";
|
||||
import {createAjvFactory, mergeArr} from "../util";
|
||||
@@ -8,7 +8,7 @@ import * as RuleSchema from '../Schema/Rule.json';
|
||||
import Ajv from 'ajv';
|
||||
import {RuleJson, RuleObjectJson} from "../Common/types";
|
||||
|
||||
export class RuleSet implements IRuleSet, Triggerable {
|
||||
export class RuleSet implements IRuleSet {
|
||||
rules: Rule[] = [];
|
||||
condition: JoinOperands;
|
||||
logger: Logger;
|
||||
@@ -32,12 +32,12 @@ export class RuleSet implements IRuleSet, Triggerable {
|
||||
}
|
||||
}
|
||||
|
||||
async run(item: Comment | Submission, existingResults: RuleResult[] = []): Promise<[boolean, RuleResult[]]> {
|
||||
async run(item: Comment | Submission, existingResults: RuleResult[] = []): Promise<[boolean, RuleSetResult]> {
|
||||
let results: RuleResult[] = [];
|
||||
let runOne = false;
|
||||
for (const r of this.rules) {
|
||||
const combinedResults = [...existingResults, ...results];
|
||||
const [passed, [result]] = await r.run(item, combinedResults);
|
||||
const [passed, result] = await r.run(item, combinedResults);
|
||||
//results = results.concat(determineNewResults(combinedResults, result));
|
||||
results.push(result);
|
||||
// skip rule if author check failed
|
||||
@@ -47,22 +47,30 @@ export class RuleSet implements IRuleSet, Triggerable {
|
||||
runOne = true;
|
||||
if (passed) {
|
||||
if (this.condition === 'OR') {
|
||||
return [true, results];
|
||||
return [true, this.generateResultSet(true, results)];
|
||||
}
|
||||
} else if (this.condition === 'AND') {
|
||||
return [false, results];
|
||||
return [false, this.generateResultSet(false, results)];
|
||||
}
|
||||
}
|
||||
// if no rules were run it's the same as if nothing was triggered
|
||||
if (!runOne) {
|
||||
return [false, results];
|
||||
return [false, this.generateResultSet(false, results)];
|
||||
}
|
||||
if(this.condition === 'OR') {
|
||||
// if OR and did not return already then none passed
|
||||
return [false, results];
|
||||
return [false, this.generateResultSet(false, results)];
|
||||
}
|
||||
// otherwise AND and did not return already so all passed
|
||||
return [true, results];
|
||||
return [true, this.generateResultSet(true, results)];
|
||||
}
|
||||
|
||||
generateResultSet(triggered: boolean, results: RuleResult[]): RuleSetResult {
|
||||
return {
|
||||
results,
|
||||
triggered,
|
||||
condition: this.condition
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export class AttributionRule extends SubmissionRule {
|
||||
}
|
||||
}
|
||||
|
||||
protected async process(item: Submission): Promise<[boolean, RuleResult[]]> {
|
||||
protected async process(item: Submission): Promise<[boolean, RuleResult]> {
|
||||
const referenceUrl = await item.url;
|
||||
if (referenceUrl === undefined && this.useSubmissionAsReference) {
|
||||
throw new Error(`Cannot run Rule ${this.name} because submission is not a link`);
|
||||
@@ -236,15 +236,15 @@ export class AttributionRule extends SubmissionRule {
|
||||
|
||||
const result = `${triggeredDomains.length} Attribution(s) met the threshold of ${threshold}, largest being ${largestCount} (${largestPercent}%) of ${activityTotal} Total -- window: ${data.window}`;
|
||||
this.logger.verbose(result);
|
||||
return Promise.resolve([true, [this.getResult(true, {
|
||||
return Promise.resolve([true, this.getResult(true, {
|
||||
result,
|
||||
data,
|
||||
})]]);
|
||||
})]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -81,11 +81,11 @@ export class RepeatActivityRule extends SubmissionRule {
|
||||
}
|
||||
}
|
||||
|
||||
async process(item: Submission): Promise<[boolean, RuleResult[]]> {
|
||||
async process(item: Submission): Promise<[boolean, RuleResult]> {
|
||||
const referenceUrl = await item.url;
|
||||
if (referenceUrl === undefined && this.useSubmissionAsReference) {
|
||||
this.logger.warn(`Rule not triggered because useSubmissionAsReference=true but submission is not a link`);
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
|
||||
let activities: (Submission | Comment)[] = [];
|
||||
@@ -193,7 +193,7 @@ export class RepeatActivityRule extends SubmissionRule {
|
||||
const largestRepeat = triggeringSummaries.reduce((acc, summ) => Math.max(summ.largestTrigger, acc), 0);
|
||||
const result = `${triggeringSummaries.length} of ${identifiersSummary.length} unique items repeated ${this.threshold} (threshold) times, largest repeat: ${largestRepeat}`;
|
||||
this.logger.verbose(result);
|
||||
return Promise.resolve([true, [this.getResult(true, {
|
||||
return Promise.resolve([true, this.getResult(true, {
|
||||
result,
|
||||
data: {
|
||||
window: typeof this.window === 'number' ? `${activities.length} Items` : activityWindowText(activities),
|
||||
@@ -204,10 +204,10 @@ export class RepeatActivityRule extends SubmissionRule {
|
||||
url: referenceUrl,
|
||||
triggeringSummaries,
|
||||
}
|
||||
})]]);
|
||||
})]);
|
||||
}
|
||||
|
||||
return Promise.resolve([false, [this.getResult(false)]]);
|
||||
return Promise.resolve([false, this.getResult(false)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,18 @@ export interface RuleResult extends ResultContext {
|
||||
triggered: (boolean | null)
|
||||
}
|
||||
|
||||
export interface RuleSetResult {
|
||||
results: RuleResult[],
|
||||
condition: 'OR' | 'AND',
|
||||
triggered: boolean
|
||||
}
|
||||
|
||||
export const isRuleSetResult = (obj: any): obj is RuleSetResult => {
|
||||
return typeof obj === 'object' && Array.isArray(obj.results) && obj.condition !== undefined && obj.triggered !== undefined;
|
||||
}
|
||||
|
||||
export interface Triggerable {
|
||||
run(item: Comment | Submission, existingResults: RuleResult[]): Promise<[(boolean | null), RuleResult[]]>;
|
||||
run(item: Comment | Submission, existingResults: RuleResult[]): Promise<[(boolean | null), RuleResult?]>;
|
||||
}
|
||||
|
||||
export abstract class Rule implements IRule, Triggerable {
|
||||
@@ -66,16 +76,16 @@ export abstract class Rule implements IRule, Triggerable {
|
||||
this.logger = logger.child({labels: ['Rule',`${this.getRuleUniqueName()}`]}, mergeArr);
|
||||
}
|
||||
|
||||
async run(item: Comment | Submission, existingResults: RuleResult[] = []): Promise<[(boolean | null), RuleResult[]]> {
|
||||
async run(item: Comment | Submission, existingResults: RuleResult[] = []): Promise<[(boolean | null), RuleResult]> {
|
||||
const existingResult = findResultByPremise(this.getPremise(), existingResults);
|
||||
if (existingResult) {
|
||||
this.logger.debug(`Returning existing result of ${existingResult.triggered ? '✔️' : '❌'}`);
|
||||
return Promise.resolve([existingResult.triggered, [{...existingResult, name: this.name}]]);
|
||||
return Promise.resolve([existingResult.triggered, {...existingResult, name: this.name}]);
|
||||
}
|
||||
const [itemPass, crit] = isItem(item, this.itemIs, this.logger);
|
||||
if(!itemPass) {
|
||||
this.logger.verbose(`Item did not pass 'itemIs' test, rule running skipped`);
|
||||
return Promise.resolve([null, [this.getResult(null, {result: `Item did not pass 'itemIs' test, rule running skipped`})]]);
|
||||
this.logger.verbose(`(Skipped) Item did not pass 'itemIs' test`);
|
||||
return Promise.resolve([null, this.getResult(null, {result: `Item did not pass 'itemIs' test`})]);
|
||||
}
|
||||
if (this.authorIs.include !== undefined && this.authorIs.include.length > 0) {
|
||||
for (const auth of this.authorIs.include) {
|
||||
@@ -83,8 +93,8 @@ export abstract class Rule implements IRule, Triggerable {
|
||||
return this.process(item);
|
||||
}
|
||||
}
|
||||
this.logger.verbose('Inclusive author criteria not matched, rule running skipped');
|
||||
return Promise.resolve([null, [this.getResult(null, {result: 'Inclusive author criteria not matched, rule running skipped'})]]);
|
||||
this.logger.verbose('(Skipped) Inclusive author criteria not matched');
|
||||
return Promise.resolve([null, this.getResult(null, {result: 'Inclusive author criteria not matched'})]);
|
||||
}
|
||||
if (this.authorIs.exclude !== undefined && this.authorIs.exclude.length > 0) {
|
||||
for (const auth of this.authorIs.exclude) {
|
||||
@@ -92,13 +102,13 @@ export abstract class Rule implements IRule, Triggerable {
|
||||
return this.process(item);
|
||||
}
|
||||
}
|
||||
this.logger.verbose('Exclusive author criteria not matched, rule running skipped');
|
||||
return Promise.resolve([null, [this.getResult(null, {result: 'Exclusive author criteria not matched, rule running skipped'})]]);
|
||||
this.logger.verbose('(Skipped) Exclusive author criteria not matched');
|
||||
return Promise.resolve([null, this.getResult(null, {result: 'Exclusive author criteria not matched'})]);
|
||||
}
|
||||
return this.process(item);
|
||||
}
|
||||
|
||||
protected abstract process(item: Comment | Submission): Promise<[boolean, RuleResult[]]>;
|
||||
protected abstract process(item: Comment | Submission): Promise<[boolean, RuleResult]>;
|
||||
|
||||
abstract getKind(): string;
|
||||
|
||||
|
||||
@@ -516,6 +516,8 @@ export const isItem = (item: Submission | Comment, stateCriteria: TypedActivityS
|
||||
if (item[k] !== undefined) {
|
||||
// @ts-ignore
|
||||
if (item[k] !== crit[k]) {
|
||||
// @ts-ignore
|
||||
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${item[k]}`)
|
||||
return [false, crit];
|
||||
}
|
||||
} else {
|
||||
@@ -523,7 +525,7 @@ export const isItem = (item: Submission | Comment, stateCriteria: TypedActivityS
|
||||
}
|
||||
}
|
||||
}
|
||||
log.verbose(`itemIs passed: ${JSON.stringify(crit)}`);
|
||||
log.debug(`Passed: ${JSON.stringify(crit)}`);
|
||||
return [true, crit];
|
||||
})() as [boolean, SubmissionState|CommentState|undefined];
|
||||
if (pass) {
|
||||
|
||||
21
src/util.ts
21
src/util.ts
@@ -1,7 +1,7 @@
|
||||
import winston, {Logger} from "winston";
|
||||
import jsonStringify from 'safe-stable-stringify';
|
||||
import dayjs, {Dayjs, OpUnitType} from 'dayjs';
|
||||
import {RulePremise, RuleResult} from "./Rule";
|
||||
import {isRuleSetResult, RulePremise, RuleResult, RuleSetResult} from "./Rule";
|
||||
import deepEqual from "fast-deep-equal";
|
||||
import {Duration} from 'dayjs/plugin/duration.js';
|
||||
import Ajv from "ajv";
|
||||
@@ -203,6 +203,25 @@ export const ruleNamesFromResults = (results: RuleResult[]) => {
|
||||
return results.map(x => x.name || x.premise.kind).join(' | ')
|
||||
}
|
||||
|
||||
export const triggeredIndicator = (val: boolean | null): string => {
|
||||
if(val === null) {
|
||||
return '-';
|
||||
}
|
||||
return val ? '✔' : '❌';
|
||||
}
|
||||
|
||||
export const resultsSummary = (results: (RuleResult|RuleSetResult)[], topLevelCondition: 'OR' | 'AND'): string => {
|
||||
const parts: string[] = results.map((x) => {
|
||||
if(isRuleSetResult(x)) {
|
||||
return `${triggeredIndicator(x.triggered)} (${resultsSummary(x.results, x.condition)}${x.results.length === 1 ? ` [${x.condition}]` : ''})`;
|
||||
}
|
||||
const res = x as RuleResult;
|
||||
return `${triggeredIndicator(x.triggered)} ${res.name}`;
|
||||
});
|
||||
return parts.join(` ${topLevelCondition} `)
|
||||
//return results.map(x => x.name || x.premise.kind).join(' | ')
|
||||
}
|
||||
|
||||
export const createAjvFactory = (logger: Logger) => {
|
||||
return new Ajv({logger: logger, verbose: true, strict: "log", allowUnionTypes: true});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user