@appland/scanner 1.78.0 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [@appland/scanner-v1.80.0](https://github.com/getappmap/appmap-js/compare/@appland/scanner-v1.79.0...@appland/scanner-v1.80.0) (2023-07-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Make sure to not process the same appmap multiple times ([da0a0d8](https://github.com/getappmap/appmap-js/commit/da0a0d8826844b4bd92bb671a3ea1b74a5563cb1))
7
+
8
+
9
+ ### Features
10
+
11
+ * upgrade @appland/models to 2.6.3 ([6e31f9c](https://github.com/getappmap/appmap-js/commit/6e31f9cc179ac0edfcde2861b937cd104ed4c687))
12
+
13
+ # [@appland/scanner-v1.79.0](https://github.com/getappmap/appmap-js/compare/@appland/scanner-v1.78.0...@appland/scanner-v1.79.0) (2023-06-23)
14
+
15
+
16
+ ### Features
17
+
18
+ * Modified dates on Finding ([b67d766](https://github.com/getappmap/appmap-js/commit/b67d766722f7d9101f295c7e5fddad5b83e9e032))
19
+
1
20
  # [@appland/scanner-v1.78.0](https://github.com/getappmap/appmap-js/compare/@appland/scanner-v1.77.2...@appland/scanner-v1.78.0) (2023-06-02)
2
21
 
3
22
 
@@ -49,6 +49,8 @@ const telemetry_1 = __importDefault(require("../../telemetry"));
49
49
  const events_1 = __importDefault(require("events"));
50
50
  const watchScanTelemetry_1 = require("./watchScanTelemetry");
51
51
  const isAncestorPath_1 = __importDefault(require("../../util/isAncestorPath"));
52
+ const util_1 = require("util");
53
+ const debug = (0, util_1.debuglog)('scanner:watch');
52
54
  function isDir(targetPath) {
53
55
  return __awaiter(this, void 0, void 0, function* () {
54
56
  try {
@@ -76,6 +78,7 @@ class Watcher {
76
78
  // do not remove callbackify, apparently on windows
77
79
  // passing plain async function doesn't work (?)
78
80
  this.queue = (0, async_1.queue)((0, node_util_1.callbackify)(this.scan.bind(this)), 2);
81
+ this.processing = new Set();
79
82
  watchScanTelemetry_1.WatchScanTelemetry.watch(this.scanEventEmitter, options.appmapDir);
80
83
  this.queue.error((error, task) => console.warn(`Problem processing ${task}:\n`, error));
81
84
  }
@@ -165,8 +168,9 @@ class Watcher {
165
168
  });
166
169
  }
167
170
  enqueue(mtimePath) {
168
- if ([...this.queue].includes(mtimePath))
171
+ if (this.processing.has(mtimePath))
169
172
  return;
173
+ this.processing.add(mtimePath);
170
174
  this.queue.push(mtimePath);
171
175
  }
172
176
  scan(mtimePath) {
@@ -177,9 +181,11 @@ class Watcher {
177
181
  const [appmapStats, reportStats] = yield Promise.all([appmapFile, reportFile].map((f) => (0, promises_1.stat)(f).catch(() => null)));
178
182
  if (!appmapStats)
179
183
  return;
184
+ const cut = (str) => str.substring(str.length - 8);
185
+ debug('%s: %s, findings: %s, config: %s', appmapFile, cut(appmapStats.mtimeMs.toFixed(3)), reportStats && cut(reportStats.mtimeMs.toFixed(3)), cut(this.config.timestampMs.toFixed(3)));
180
186
  if (reportStats &&
181
- reportStats.mtimeMs > appmapStats.mtimeMs &&
182
- reportStats.mtimeMs > this.config.timestampMs)
187
+ reportStats.mtimeMs > appmapStats.mtimeMs - 1000 &&
188
+ reportStats.mtimeMs > this.config.timestampMs - 1000)
183
189
  return; // report is up to date
184
190
  const startTime = Date.now();
185
191
  const scanner = yield (0, scanner_1.default)(true, this.config, [appmapFile]);
@@ -188,6 +194,7 @@ class Watcher {
188
194
  this.scanEventEmitter.emit('scan', { scanResults: rawScanResults, elapsed });
189
195
  // Always report the raw data
190
196
  yield (0, promises_1.writeFile)(reportFile, (0, formatReport_1.formatReport)(rawScanResults));
197
+ this.processing.delete(mtimePath);
191
198
  });
192
199
  }
193
200
  reloadConfig() {
@@ -15,20 +15,8 @@ const src_1 = require("@appland/client/dist/src");
15
15
  const util_1 = require("../rules/lib/util");
16
16
  const create_1 = require("../integration/appland/scannerJob/create");
17
17
  const vars_1 = require("../integration/vars");
18
- const promises_2 = require("fs/promises");
19
18
  const path_1 = require("path");
20
19
  const pruneAppMap_1 = require("./upload/pruneAppMap");
21
- function fileExists(file) {
22
- return __awaiter(this, void 0, void 0, function* () {
23
- try {
24
- yield (0, promises_2.stat)(file);
25
- return true;
26
- }
27
- catch (e) {
28
- return false;
29
- }
30
- });
31
- }
32
20
  function create(scanResults, appId, appMapDir, mergeKey, mapsetOptions = {}, retryOptions = {}) {
33
21
  return __awaiter(this, void 0, void 0, function* () {
34
22
  if ((0, util_1.verbose)())
@@ -47,7 +35,7 @@ function create(scanResults, appId, appMapDir, mergeKey, mapsetOptions = {}, ret
47
35
  if ((0, util_1.verbose)())
48
36
  console.log(`Uploading AppMap ${filePath}`);
49
37
  const filePaths = [filePath, (0, path_1.join)(appMapDir, filePath)];
50
- const filePathsExist = yield Promise.all(filePaths.map(fileExists));
38
+ const filePathsExist = yield Promise.all(filePaths.map(util_1.fileExists));
51
39
  const fullPath = filePaths.find((_, fileIndex) => filePathsExist[fileIndex]);
52
40
  if (!fullPath)
53
41
  throw new Error(`File ${filePath} not found`);
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const octokit_1 = require("octokit");
3
4
  const vars_1 = require("../vars");
4
5
  function postCommitStatus(state, description) {
5
6
  (0, vars_1.validateToken)();
@@ -7,9 +8,11 @@ function postCommitStatus(state, description) {
7
8
  (0, vars_1.validateOwner)();
8
9
  (0, vars_1.validateSha)();
9
10
  // eslint-disable-next-line @typescript-eslint/no-var-requires
10
- const octokat = require('octokat');
11
- const octo = new octokat({ token: (0, vars_1.token)() });
12
- return octo.repos((0, vars_1.owner)(), (0, vars_1.repo)()).statuses((0, vars_1.sha)()).create({
11
+ const octo = new octokit_1.Octokit({ auth: (0, vars_1.token)() });
12
+ return octo.rest.repos.createCommitStatus({
13
+ owner: (0, vars_1.owner)(),
14
+ repo: (0, vars_1.repo)(),
15
+ sha: (0, vars_1.sha)(),
13
16
  state: state,
14
17
  context: 'appland/scanner',
15
18
  description: description,
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.fileModifiedDate = exports.gitModifiedDate = exports.gitExists = exports.isCached = exports.resetCache = void 0;
13
+ const console_1 = require("console");
14
+ const util_1 = require("./rules/lib/util");
15
+ const child_process_1 = require("child_process");
16
+ const promises_1 = require("fs/promises");
17
+ const FileModifiedDate = new Map();
18
+ let GitExists;
19
+ function resetCache() {
20
+ FileModifiedDate.clear();
21
+ }
22
+ exports.resetCache = resetCache;
23
+ function isCached(file) {
24
+ return FileModifiedDate.has(file);
25
+ }
26
+ exports.isCached = isCached;
27
+ function detectGitExists() {
28
+ return new Promise((resolve) => {
29
+ (0, child_process_1.exec)('git --version', (err) => {
30
+ if (err && err.code && err.code > 0)
31
+ resolve(false);
32
+ resolve(true);
33
+ });
34
+ });
35
+ }
36
+ function gitExists() {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ if (GitExists === undefined) {
39
+ GitExists = yield detectGitExists();
40
+ }
41
+ return GitExists;
42
+ });
43
+ }
44
+ exports.gitExists = gitExists;
45
+ function gitModifiedDate(file) {
46
+ return new Promise((resolve) => {
47
+ (0, child_process_1.exec)(`git log -n 1 --pretty=format:%cI ${file}`, (err, stdout) => {
48
+ if (err && err.code && err.code > 0)
49
+ resolve(undefined);
50
+ if (stdout.trim() === '')
51
+ resolve(undefined);
52
+ resolve(new Date(stdout));
53
+ });
54
+ });
55
+ }
56
+ exports.gitModifiedDate = gitModifiedDate;
57
+ function fileModifiedDate(file) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ try {
60
+ const stats = yield (0, promises_1.stat)(file);
61
+ return stats.mtime;
62
+ }
63
+ catch (e) {
64
+ (0, console_1.warn)(e);
65
+ }
66
+ });
67
+ }
68
+ exports.fileModifiedDate = fileModifiedDate;
69
+ function lastGitOrFSModifiedDate(file) {
70
+ return __awaiter(this, void 0, void 0, function* () {
71
+ let result = FileModifiedDate.get(file);
72
+ if (result) {
73
+ if ((0, util_1.verbose)())
74
+ (0, console_1.debug)(`Using cached modified date for ${file}`);
75
+ return result.getTime() === 0 ? undefined : result;
76
+ }
77
+ if ((0, util_1.verbose)())
78
+ (0, console_1.debug)(`Computing modified date for ${file}`);
79
+ if (yield gitExists())
80
+ result = yield gitModifiedDate(file);
81
+ if (!result)
82
+ result = yield fileModifiedDate(file);
83
+ FileModifiedDate.set(file, result || new Date(0));
84
+ return result;
85
+ });
86
+ }
87
+ exports.default = lastGitOrFSModifiedDate;
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ const models_1 = require("@appland/models");
15
16
  const errors_1 = require("./errors");
16
17
  const util_1 = require("./rules/lib/util");
17
18
  const rootScope_1 = __importDefault(require("./scope/rootScope"));
@@ -23,6 +24,18 @@ const checkInstance_1 = __importDefault(require("./checkInstance"));
23
24
  const eventUtil_1 = require("./eventUtil");
24
25
  const hashV1_1 = __importDefault(require("./algorithms/hash/hashV1"));
25
26
  const hashV2_1 = __importDefault(require("./algorithms/hash/hashV2"));
27
+ const path_1 = require("path");
28
+ const lastGitOrFSModifiedDate_1 = __importDefault(require("./lastGitOrFSModifiedDate"));
29
+ const console_1 = require("console");
30
+ const assert_1 = __importDefault(require("assert"));
31
+ function locationToFilePath(location) {
32
+ const [file] = location.split(':');
33
+ let filePath = file;
34
+ if ((0, path_1.isAbsolute)(file) && file.startsWith(process.cwd())) {
35
+ filePath = file.slice(process.cwd().length + 1);
36
+ }
37
+ return filePath;
38
+ }
26
39
  class RuleChecker {
27
40
  constructor(progress) {
28
41
  this.progress = progress;
@@ -93,10 +106,44 @@ class RuleChecker {
93
106
  if (!checkInstance.filterEvent(event, appMapIndex)) {
94
107
  return;
95
108
  }
109
+ let appmapConfigDir;
110
+ {
111
+ let searchDir = (0, path_1.dirname)((0, path_1.resolve)(appMapFileName));
112
+ while (!appmapConfigDir) {
113
+ if (yield (0, util_1.fileExists)((0, path_1.join)(searchDir, 'appmap.yml'))) {
114
+ appmapConfigDir = searchDir;
115
+ }
116
+ else {
117
+ if ((0, path_1.dirname)(searchDir) === searchDir)
118
+ break;
119
+ searchDir = (0, path_1.dirname)(searchDir);
120
+ }
121
+ }
122
+ }
123
+ const resolvePath = (path) => __awaiter(this, void 0, void 0, function* () {
124
+ const candidates = [path];
125
+ if (appmapConfigDir)
126
+ candidates.push((0, path_1.join)(appmapConfigDir, path));
127
+ for (const candidate of candidates)
128
+ if (yield (0, util_1.fileExists)(candidate))
129
+ return candidate;
130
+ });
131
+ const mostRecentModifiedDate = (filePaths) => __awaiter(this, void 0, void 0, function* () {
132
+ const dates = new Array();
133
+ for (const filePath of filePaths) {
134
+ const resolvedPath = yield resolvePath(filePath);
135
+ if (!resolvedPath)
136
+ continue;
137
+ const date = yield (0, lastGitOrFSModifiedDate_1.default)(resolvedPath);
138
+ if (date)
139
+ dates.push(date);
140
+ }
141
+ return dates.sort((a, b) => (a && b ? b.getTime() - a.getTime() : 0))[0];
142
+ });
96
143
  const buildFinding = (matchEvent, participatingEvents, message, groupMessage, occurranceCount,
97
144
  // matchEvent will be added to additionalEvents and participatingEvents.values
98
145
  // to create the relatedEvents array
99
- additionalEvents) => {
146
+ additionalEvents) => __awaiter(this, void 0, void 0, function* () {
100
147
  const findingEvent = matchEvent || event;
101
148
  // Fixes:
102
149
  // TypeError: Cannot read property 'forEach' of undefined
@@ -112,18 +159,56 @@ class RuleChecker {
112
159
  // findingEvent gets passed here as a relatedEvent, and if you look at HashV1 it
113
160
  // gets added to the hash again. That's how it worked in V1 so it's here for compatibility.
114
161
  additionalEvents || []);
162
+ let scopeModifiedDate;
163
+ {
164
+ const scopeNavigator = new models_1.EventNavigator(scope);
165
+ const scopeFiles = new Set();
166
+ const collectScope = (event) => {
167
+ if (!event.codeObject.location)
168
+ return;
169
+ const filePath = locationToFilePath(event.codeObject.location);
170
+ if (!filePath)
171
+ return;
172
+ scopeFiles.add(filePath);
173
+ };
174
+ collectScope(scope);
175
+ for (const descendant of scopeNavigator.descendants()) {
176
+ const { event } = descendant;
177
+ collectScope(event);
178
+ }
179
+ const localScopeFiles = [...scopeFiles].filter((filePath) => ((0, assert_1.default)(filePath), !(0, path_1.isAbsolute)(filePath)));
180
+ scopeModifiedDate = yield mostRecentModifiedDate(localScopeFiles);
181
+ }
115
182
  const hashV2 = new hashV2_1.default(checkInstance.ruleId, findingEvent, participatingEvents);
116
183
  const uniqueEvents = new Set();
117
184
  const relatedEvents = [];
118
- [findingEvent, ...(additionalEvents || []), ...Object.values(participatingEvents)]
119
- .map(eventUtil_1.cloneEvent)
120
- .forEach((event) => {
185
+ const relatedEventFiles = new Set();
186
+ const collectEventFile = (event) => {
187
+ if (!event.codeObject.location)
188
+ return;
189
+ const filePath = locationToFilePath(event.codeObject.location);
190
+ if (!filePath)
191
+ return;
192
+ if ((0, path_1.isAbsolute)(filePath))
193
+ return;
194
+ relatedEventFiles.add(filePath);
195
+ };
196
+ [findingEvent, ...(additionalEvents || []), ...Object.values(participatingEvents)].forEach((event) => {
121
197
  if (uniqueEvents.has(event.id)) {
122
198
  return;
123
199
  }
200
+ collectEventFile(event);
201
+ for (const ancestor of new models_1.EventNavigator(event).ancestors()) {
202
+ collectEventFile(ancestor.event);
203
+ }
124
204
  uniqueEvents.add(event.id);
125
205
  relatedEvents.push((0, eventUtil_1.cloneEvent)(event));
126
206
  });
207
+ const eventsModifiedDate = yield mostRecentModifiedDate([...relatedEventFiles]);
208
+ if ((0, util_1.verbose)()) {
209
+ (0, console_1.warn)(`Scope modified date: ${scopeModifiedDate}`);
210
+ (0, console_1.warn)(`Events modified date: ${eventsModifiedDate}`);
211
+ }
127
212
  return {
128
213
  appMapFile: appMapFileName,
129
214
  checkId: checkInstance.checkId,
@@ -140,8 +225,10 @@ class RuleChecker {
140
225
  relatedEvents: relatedEvents.sort((event) => event.id),
141
226
  impactDomain: checkInstance.checkImpactDomain,
142
227
  participatingEvents: Object.fromEntries(Object.entries(participatingEvents).map(([k, v]) => [k, (0, eventUtil_1.cloneEvent)(v)])),
228
+ scopeModifiedDate,
229
+ eventsModifiedDate,
143
230
  };
144
- };
231
+ });
145
232
  if (this.progress)
146
233
  yield this.progress.matchEvent(event, appMapIndex);
147
234
  const matchResult = yield checkInstance.ruleLogic.matcher(event, appMapIndex, checkInstance.filterEvent.bind(checkInstance));
@@ -152,23 +239,23 @@ class RuleChecker {
152
239
  let finding;
153
240
  if (checkInstance.ruleLogic.message) {
154
241
  const message = checkInstance.ruleLogic.message(scope, event);
155
- finding = buildFinding(event, {}, message);
242
+ finding = yield buildFinding(event, {}, message);
156
243
  }
157
244
  else {
158
- finding = buildFinding(event, {});
245
+ finding = yield buildFinding(event, {});
159
246
  }
160
247
  findings.push(finding);
161
248
  }
162
249
  else if (typeof matchResult === 'string') {
163
- const finding = buildFinding(event, {}, matchResult);
250
+ const finding = yield buildFinding(event, {}, matchResult);
164
251
  finding.message = matchResult;
165
252
  findings.push(finding);
166
253
  }
167
254
  else if (matchResult) {
168
- matchResult.forEach((mr) => {
169
- const finding = buildFinding(mr.event, mr.participatingEvents || {}, mr.message, mr.groupMessage, mr.occurranceCount, mr.relatedEvents);
255
+ for (const mr of matchResult) {
256
+ const finding = yield buildFinding(mr.event, mr.participatingEvents || {}, mr.message, mr.groupMessage, mr.occurranceCount, mr.relatedEvents);
170
257
  findings.push(finding);
171
- });
258
+ }
172
259
  }
173
260
  if ((0, util_1.verbose)()) {
174
261
  if (findings.length > numFindings) {
@@ -12,11 +12,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.verbose = exports.toRegExpArray = exports.responseContentType = exports.toRegExp = exports.providesAuthentication = exports.pluralize = exports.dasherize = exports.camelize = exports.parseValue = exports.isRoot = exports.ideLink = exports.isTruthy = exports.isFalsey = exports.emptyValue = exports.capitalize = exports.appMapDir = exports.collectAppMapFiles = void 0;
15
+ exports.verbose = exports.toRegExpArray = exports.responseContentType = exports.toRegExp = exports.providesAuthentication = exports.pluralize = exports.dasherize = exports.camelize = exports.parseValue = exports.isRoot = exports.ideLink = exports.isTruthy = exports.isFalsey = exports.fileExists = exports.emptyValue = exports.capitalize = exports.appMapDir = exports.collectAppMapFiles = void 0;
16
16
  const path_1 = require("path");
17
17
  const util_1 = require("util");
18
18
  const glob_1 = require("glob");
19
19
  const assert_1 = __importDefault(require("assert"));
20
+ const promises_1 = require("fs/promises");
20
21
  function collectAppMapFiles(appmapFile, appmapDir) {
21
22
  return __awaiter(this, void 0, void 0, function* () {
22
23
  let files = [];
@@ -164,3 +165,15 @@ function pluralize(word, count) {
164
165
  return count === 1 ? word : [word, 's'].join('');
165
166
  }
166
167
  exports.pluralize = pluralize;
168
+ function fileExists(file) {
169
+ return __awaiter(this, void 0, void 0, function* () {
170
+ try {
171
+ yield (0, promises_1.stat)(file);
172
+ return true;
173
+ }
174
+ catch (e) {
175
+ return false;
176
+ }
177
+ });
178
+ }
179
+ exports.fileExists = fileExists;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appland/scanner",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Analyze AppMaps for code flaws",
5
5
  "bin": "built/cli.js",
6
6
  "files": [
@@ -20,12 +20,14 @@
20
20
  "lint": "eslint src --ext .ts",
21
21
  "ci": "yarn lint && yarn build && yarn schema-up-to-date && yarn doc-up-to-date && yarn test",
22
22
  "test": "jest --filter=./test/testFilter.js",
23
+ "jest": "jest --filter=./test/testFilter.js",
23
24
  "semantic-release": "semantic-release",
24
25
  "watch": "node bin/preBuild.js && tsc -p tsconfig.build.json --watch"
25
26
  },
26
27
  "author": "AppLand, Inc.",
27
28
  "license": "Commons Clause + MIT",
28
29
  "devDependencies": {
30
+ "@appland/appmap-agent-js": "^13.9.0",
29
31
  "@semantic-release/changelog": "^6.0.1",
30
32
  "@semantic-release/git": "^10.0.1",
31
33
  "@types/async": "^3.2.12",
@@ -61,25 +63,25 @@
61
63
  },
62
64
  "dependencies": {
63
65
  "@appland/client": "^1.5.0",
64
- "@appland/models": "^2.6.2",
65
- "@appland/openapi": "1.5.0",
66
+ "@appland/models": "^2.6.3",
67
+ "@appland/openapi": "1.6.0",
66
68
  "@appland/sql-parser": "^1.5.0",
67
69
  "@types/cli-progress": "^3.9.2",
68
70
  "ajv": "^8.8.2",
69
71
  "applicationinsights": "^2.1.4",
70
- "async": "^3.2.3",
72
+ "async": "^3.2.4",
71
73
  "boxen": "^5.0.1",
72
74
  "chalk": "^4.1.2",
73
75
  "chokidar": "^3.5.1",
74
- "cli-progress": "^3.11.0",
75
- "conf": "^10.0.2",
76
+ "cli-progress": "^3.12.0",
77
+ "conf": "10.2.0",
76
78
  "crypto-js": "^4.0.0",
77
79
  "glob": "7.2.3",
78
80
  "inquirer": "^8.1.2",
79
81
  "js-yaml": "^4.1.0",
80
82
  "lru-cache": "^6.0.0",
81
83
  "minimatch": "^5.1.2",
82
- "octokat": "^0.10.0",
84
+ "octokit": "^2.0.19",
83
85
  "openapi-diff": "^0.23.5",
84
86
  "ora": "~5",
85
87
  "pretty-format": "^27.4.6",
package/src/types.d.ts CHANGED
@@ -116,6 +116,8 @@ interface Finding {
116
116
  impactDomain?: ImpactDomain;
117
117
  // Map of events by functional role name; for example, logEvent, secret, scope, etc.
118
118
  participatingEvents?: Record<string, Event>;
119
+ scopeModifiedDate?: Date;
120
+ eventsModifiedDate?: Date;
119
121
  }
120
122
 
121
123
  interface RuleLogic {