@jrpool/kilotest 42.0.6 → 43.0.1

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.
@@ -15,7 +15,7 @@ const {
15
15
  isHidden,
16
16
  tools
17
17
  } = require('../util');
18
- const {issues} = require('testilo/procs/score/tic');
18
+ const {issues: issueSpecs} = require('testaro-issues');
19
19
  const fs = require('fs/promises');
20
20
  const path = require('path');
21
21
 
@@ -62,7 +62,7 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
62
62
  query.target = what;
63
63
  query.urlLink = urlLink;
64
64
  query.testInfo = testInfo;
65
- query.issue = issues[issueID]?.summary;
65
+ query.issue = issueSpecs[issueID]?.summary;
66
66
  // If adding the issue summary failed:
67
67
  if (!query.issue) {
68
68
  // Populate the query with the reason.
@@ -71,7 +71,7 @@ const populateQuery = async (issueID, timeStamp, jobID, catalogIndex, pathID, qu
71
71
  return;
72
72
  }
73
73
  // Otherwise, i.e. if it succeeded, get the issue details.
74
- const issue = issues[issueID];
74
+ const issue = issueSpecs[issueID];
75
75
  const {wcag, weight, why} = issue;
76
76
  query.why = why;
77
77
  query.priority = getWeightName(weight);
package/index.js CHANGED
@@ -26,8 +26,7 @@ const {
26
26
  jobsPath,
27
27
  makeReportsExtract,
28
28
  recsLock,
29
- reportsPath,
30
- ruleIDs
29
+ reportsPath
31
30
  } = require('./util');
32
31
  const {handleMCP, mcpPath} = require('./mcp');
33
32
  const fs = require('fs/promises');
@@ -750,7 +749,7 @@ const requestHandler = async (request, response) => {
750
749
  // Update the data on the available reports.
751
750
  await makeReportsExtract();
752
751
  // Annotate the report.
753
- await annotateReport(ruleIDs, timeStamp, jobID);
752
+ await annotateReport(timeStamp, jobID);
754
753
  console.log(`Testaro report ${id} was annotated, saved, and indexed`);
755
754
  // Check the monetary balances and send alerts if nearing exhaustion.
756
755
  await checkBalancesForAlerts(report);
package/issues/index.js CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  getWeightName,
15
15
  objectSort,
16
16
  } = require('../util');
17
- const {issues} = require('testilo/procs/score/tic');
17
+ const {issues: issueSpecs} = require('testaro-issues');
18
18
  const fs = require('fs/promises');
19
19
  const path = require('path');
20
20
 
@@ -69,13 +69,13 @@ const getIssuesSummary = async () => {
69
69
  Object.entries(issuesData).forEach(([issueID, data]) => {
70
70
  const {count, reporters} = data;
71
71
  // If the issue is still classified:
72
- if (issues[issueID]) {
72
+ if (issueSpecs[issueID]) {
73
73
  // Increment the report violation count by the issue violation count.
74
74
  summary.totalCount += count;
75
75
  // Add the issue data and an initilized percentage to the summary.
76
76
  summary.issues.push({
77
77
  issueID,
78
- weight: issues[issueID].weight,
78
+ weight: issueSpecs[issueID].weight,
79
79
  count,
80
80
  percentage: 0,
81
81
  reporters: getToolNamesString(reporters)
@@ -129,7 +129,7 @@ const populateQuery = async query => {
129
129
  if (reportedIssue.weight === weight && percentage >= 2) {
130
130
  existsIssue = true;
131
131
  // Get the data on it from the issue classification.
132
- const issue = issues[issueID];
132
+ const issue = issueSpecs[issueID];
133
133
  const {summary, wcag, why} = issue;
134
134
  const wcagLink = `<a href="${getWCAGLink(wcag)}">${wcag}</a>`;
135
135
  // Add a description of it to the lines.
package/llms-full.txt CHANGED
@@ -174,6 +174,6 @@ More information about Kilotest features and internals:
174
174
  - [Tutorial on authorizing language models to use Kilotest](https://kilotest.com/qai/)
175
175
  - [README file](https://github.com/jrpool/kilotest/blob/main/README.md)
176
176
  - [Testaro rule-engine orchestrator](https://github.com/jrpool/testaro)
177
- - [Testilo rule classifier](https://github.com/jrpool/testilo/blob/main/procs/score/tic.js)
177
+ - [Testaro rule classifier](https://github.com/jrpool/testaro-issues)
178
178
  - [Research report on Testaro](https://arxiv.org/abs/2309.10167)
179
179
  - [Blog post on the Kilotest use case](https://medium.com/cvs-health-tech-blog/how-to-run-a-thousand-accessibility-tests-63692ad120c3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "42.0.6",
3
+ "version": "43.0.1",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@modelcontextprotocol/sdk": "*",
37
37
  "dotenv": "*",
38
- "testilo": "*",
38
+ "testaro-issues": "*",
39
39
  "zod": "*"
40
40
  },
41
41
  "devDependencies": {
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {annotateReport, getReportExtracts, ruleIDs} = require('../util');
8
+ const {annotateReport, getReportExtracts} = require('../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -23,7 +23,7 @@ exports.answer = async authCode => {
23
23
  for (const reportExtract of reportExtracts) {
24
24
  const {timeStamp, jobID} = reportExtract;
25
25
  // Reannotate it.
26
- const annotationError = await annotateReport(ruleIDs, timeStamp, jobID);
26
+ const annotationError = await annotateReport(timeStamp, jobID);
27
27
  // If this failed:
28
28
  if (annotationError) {
29
29
  // Return an error page.
@@ -5,7 +5,7 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getIssue, getReport, getReportExtracts, ruleIDs} = require('../util');
8
+ const {getIssue, getReport, getReportExtracts} = require('../util');
9
9
  const fs = require('fs/promises');
10
10
  const path = require('path');
11
11
 
@@ -40,7 +40,7 @@ const populateQuery = async query => {
40
40
  result.standardResult.instances.forEach(instance => {
41
41
  const {ruleID} = instance;
42
42
  // Get the issue ID of the rule, or null if none.
43
- const issueID = getIssue(ruleIDs, which, ruleID);
43
+ const issueID = getIssue(which, ruleID);
44
44
  // If the issue ID of the instance differs from that of the rule:
45
45
  if ((instance.issueID || null) !== issueID) {
46
46
  // Add the rule and the report to the rules with changed issue IDs.
@@ -88,21 +88,20 @@ const populateQuery = async query => {
88
88
  });
89
89
  // Add the lines to the query.
90
90
  query.reClassified = reClassifiedLines.join('\n');
91
+ const formLines = [];
91
92
  if (reClassifiedLines.length) {
92
93
  query.how = 'Each <q>reclassified</q> rule indicates that report annotations are out of date. To update them, submit your authorization code.';
93
- const formLines = [];
94
- formLines.push(`${margin}<form action="/reannotate.html" method="post">`);
95
- formLines.push(
96
- `${margin} <p><label>Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required></label></p>`
97
- );
98
- formLines.push(`${margin} <p><button type="submit">Reannotate</button></p>`);
99
- formLines.push(`${margin}</form>`);
100
- query.reannotateForm = formLines.join('\n');
101
94
  }
102
95
  else {
103
- query.how = 'No violated rules have been classified or reclassified after being reported, so reannotation of the reports is not necessary.';
104
- query.reannotateForm = '';
96
+ query.how = 'No violated rules have been classified or reclassified after being reported, so reannotation of the reports is normally not necessary. If there is another reason for reannotation, submit your authorization code.';
105
97
  }
98
+ formLines.push(`${margin}<form action="/reannotate.html" method="post">`);
99
+ formLines.push(
100
+ `${margin} <p><label>Authorization code: <input size="3" minLength="3" maxlength="3" name="authCode" required></label></p>`
101
+ );
102
+ formLines.push(`${margin} <p><button type="submit">Reannotate</button></p>`);
103
+ formLines.push(`${margin}</form>`);
104
+ query.reannotateForm = formLines.join('\n');
106
105
  };
107
106
  // Returns a page disclosing newly classified rules and a form to reannotate reports.
108
107
  exports.answer = async () => {
@@ -17,7 +17,7 @@ const {
17
17
  isHidden,
18
18
  makeBreakable,
19
19
  } = require('../util');
20
- const {issues} = require('testilo/procs/score/tic');
20
+ const {issues: issueSpecs} = require('testaro-issues');
21
21
  const fs = require('fs/promises');
22
22
  const path = require('path');
23
23
 
@@ -35,7 +35,7 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
35
35
  return;
36
36
  }
37
37
  // Otherwise, i.e. if it succeeded, add the issue summary to the query.
38
- query.issue = issues[issueID]?.summary;
38
+ query.issue = issueSpecs[issueID]?.summary;
39
39
  // If adding the issue summary failed:
40
40
  if (!query.issue) {
41
41
  // Populate the query with the reason.
@@ -48,7 +48,7 @@ const populateQuery = async (issueID, timeStamp, jobID, query) => {
48
48
  query.target = what;
49
49
  query.urlLink = urlLink;
50
50
  query.testInfo = testInfo;
51
- const issue = issues[issueID];
51
+ const issue = issueSpecs[issueID];
52
52
  const {wcag, weight, why} = issue;
53
53
  query.why = why;
54
54
  query.priority = getWeightName(weight);
@@ -18,7 +18,7 @@ const {
18
18
  objectSort,
19
19
  tools
20
20
  } = require('../util');
21
- const issuesClassification = require('testilo/procs/score/tic').issues;
21
+ const {issues: issueSpecs} = require('testaro-issues');
22
22
  const fs = require('fs/promises');
23
23
  const path = require('path');
24
24
 
@@ -67,7 +67,7 @@ const getIssuesData = async (timeStamp, jobID) => {
67
67
  const {catalogIndex, issueID} = instance;
68
68
  // If the instance identifies its rule as belonging to a non-ignorable issue:
69
69
  if (issueID && issueID !== 'ignorable') {
70
- const issueClassification = issuesClassification[issueID];
70
+ const issueClassification = issueSpecs[issueID];
71
71
  // If the issue has a current weighted classification:
72
72
  if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
73
73
  const {summary, wcag, weight, why} = issueClassification;
package/rules/index.html CHANGED
@@ -20,9 +20,7 @@
20
20
  <li>Related WCAG standard: __wcag__</li>
21
21
  </ul>
22
22
  <h2>Rules</h2>
23
- <ul>
24
- __rules__
25
- </ul>
23
+ __rules__
26
24
  </main>
27
25
  </body>
28
26
  </html>
package/rules/index.js CHANGED
@@ -8,9 +8,9 @@
8
8
  const {
9
9
  getWeightName,
10
10
  htmlSafe,
11
- tools
11
+ ruleEngines
12
12
  } = require('../util');
13
- const {issues} = require('testilo/procs/score/tic');
13
+ const {issues: issueSpecs, issueRules, rules: ruleSpecs} = require('testaro-issues');
14
14
  const fs = require('fs/promises');
15
15
  const path = require('path');
16
16
 
@@ -19,8 +19,12 @@ const path = require('path');
19
19
  // Adds parameters to a query for the answer page.
20
20
  const populateQuery = async (issueID, query) => {
21
21
  // Add facts about the issue to the query.
22
- query.issue = issues[issueID].summary;
23
- const issue = issues[issueID];
22
+ query.issue = issueSpecs[issueID]?.summary;
23
+ if (!query.issue) {
24
+ query.error = 'Issue not found';
25
+ return;
26
+ }
27
+ const issue = issueSpecs[issueID];
24
28
  const {wcag, weight, why} = issue;
25
29
  query.why = why;
26
30
  query.priority = getWeightName(weight);
@@ -28,44 +32,61 @@ const populateQuery = async (issueID, query) => {
28
32
  // Initialize the lines.
29
33
  const lines = [];
30
34
  const margin = ' '.repeat(6);
31
- // For each tool with any rules belonging to the issue:
32
- Object.keys(issue.tools).forEach(toolID => {
33
- // Add a line.
34
- lines.push(`${margin}<li><h3>${tools[toolID][0]} rules</h3>`);
35
- lines.push(`${margin} <ul>`);
36
- const tool = issue.tools[toolID];
37
- // For each rule of the tool belonging to the issue:
38
- Object.keys(tool).forEach(ruleID => {
39
- const rule = tool[ruleID];
40
- const {what} = rule;
41
- // Add facts about the rule.
42
- if (what === ruleID) {
43
- lines.push(`${margin} <li><code>${htmlSafe(ruleID)}</code></li>`);
44
- }
45
- else {
46
- lines.push(`${margin} <li><code>${htmlSafe(ruleID)}</code>: ${htmlSafe(what)}</li>`);
35
+ // For each rule engine with any rules belonging to the issue:
36
+ Object.keys(issueRules[issueID] ?? {}).forEach(engineID => {
37
+ // Add a heading for the rules of the rule engine.
38
+ lines.push(`${margin}<h3>${ruleEngines[engineID][0]} rules</h3>`);
39
+ const {invariant, variable} = issueRules[issueID][engineID];
40
+ const rulesByType = {
41
+ invariant,
42
+ variable
43
+ };
44
+ // For each rule variability:
45
+ Object.keys(rulesByType).forEach(typeName => {
46
+ // If any rules of the rule engine belonging to the issue have it:
47
+ if (rulesByType[typeName].length) {
48
+ // Add a heading for them.
49
+ lines.push(`${margin}<h4>${typeName} rules</h4>`);
50
+ // Add a list of facts about them.
51
+ lines.push(`${margin}<ul>`);
52
+ rulesByType[typeName].forEach(ruleID => {
53
+ const {what} = ruleSpecs[engineID][typeName][ruleID];
54
+ if (what === ruleID) {
55
+ lines.push(`${margin} <li><code>${htmlSafe(ruleID)}</code></li>`);
56
+ }
57
+ else {
58
+ lines.push(`${margin} <li><code>${htmlSafe(ruleID)}</code>: ${htmlSafe(what)}</li>`);
59
+ }
60
+ });
61
+ lines.push(`${margin}</ul>`);
47
62
  }
48
63
  });
49
- lines.push(`${margin} </ul>`);
50
- lines.push(`${margin}</li>`);
51
64
  });
52
65
  // Add the lines to the query.
53
66
  query.rules = lines.join('\n');
54
67
  };
55
- // Returns a page answering the violators question.
68
+ // Returns a page answering the issue-rules question.
56
69
  exports.answer = async issueID => {
57
70
  const query = {};
58
71
  // Create a query to replace the placeholders.
59
72
  await populateQuery(issueID, query);
60
- // Get the template.
61
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
62
- // Replace its placeholders.
63
- Object.keys(query).forEach(param => {
64
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
65
- });
66
- // Return the populated page.
73
+ // If this succeeded:
74
+ if (query.issue) {
75
+ // Get the template.
76
+ let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
77
+ // Replace its placeholders.
78
+ Object.keys(query).forEach(param => {
79
+ answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
80
+ });
81
+ // Return the populated page.
82
+ return {
83
+ status: 'ok',
84
+ answerPage
85
+ };
86
+ }
87
+ // Otherwise, i.e. if it failed, report this.
67
88
  return {
68
- status: 'ok',
69
- answerPage
89
+ status: 'error',
90
+ message: query.error
70
91
  };
71
92
  };
package/util.js CHANGED
@@ -6,9 +6,7 @@
6
6
  // IMPORTS
7
7
 
8
8
  const {sendAlert} = require('./alerts');
9
- const issuesClassification
10
- = exports.issuesClassification
11
- = require('testilo/procs/score/tic').issues;
9
+ const {issues: issueSpecs, rules: ruleSpecs} = require('testaro-issues');
12
10
  const fs = require('fs/promises');
13
11
  const path = require('path');
14
12
  const querystring = require('querystring');
@@ -120,26 +118,25 @@ const getDateTime = exports.getDateTime = timeStamp => {
120
118
  return dateTime.toString() === 'Invalid Date' ? null : dateTime;
121
119
  };
122
120
  // Returns the issue that a rule belongs to, or null if none.
123
- const getIssue = exports.getIssue = (ruleIDs, toolID, ruleID) => {
124
- const {invariant, variable} = ruleIDs;
125
- // Initialize the issue ID of the rule as if the rule ID is invariant.
126
- let issueID = invariant[toolID]?.[ruleID];
127
- // If the initialization succeeded:
128
- if (issueID) {
129
- // Return it.
130
- return issueID;
121
+ const getIssue = exports.getIssue = (engineID, ruleID) => {
122
+ const engineRules = ruleSpecs[engineID];
123
+ // If the rule engine has no rule specifications:
124
+ if (!engineRules) {
125
+ // Return a failure result.
126
+ return null;
131
127
  }
132
- // Otherwise, change the rule ID to the first matching variable rule ID of the tool.
133
- ruleID = Object
134
- .keys(variable[toolID] ?? {})
135
- .find(variableRuleID => new RegExp(variableRuleID).test(ruleID));
136
- // If the change succeeded:
137
- if (ruleID) {
138
- // Return the issue ID.
139
- return variable[toolID][ruleID];
128
+ const {invariant, variable} = engineRules;
129
+ // If the rule ID is invariant and classified:
130
+ if (invariant[ruleID]) {
131
+ // Return its issue ID.
132
+ return invariant[ruleID].issueID;
140
133
  }
141
- // Otherwise, i.e. if no issue was found, return a failure result.
142
- return null;
134
+ // Otherwise, find the first matching variable rule ID pattern.
135
+ const variableRuleID = Object
136
+ .keys(variable)
137
+ .find(pattern => new RegExp(`^${pattern}$`).test(ruleID));
138
+ // Return the issue ID if a pattern matched, or a failure result otherwise.
139
+ return variableRuleID ? variable[variableRuleID].issueID : null;
143
140
  };
144
141
  // Gets the names and categories of the job files.
145
142
  const getJobNames = exports.getJobNames = async () => {
@@ -179,58 +176,6 @@ const getObject = exports.getObject = async filePath => {
179
176
  exports.getRandomString = length => {
180
177
  return Math.random().toString(36).slice(2, length + 2);
181
178
  };
182
- // Compiles a directory of the issue classifications of invariant and variable rules.
183
- const getRuleIDs = exports.getRuleIDs = () => {
184
- // Initialize data on invariant and variable rule IDs.
185
- const invariant = {};
186
- const variable = {};
187
- // Initialize a validity checker.
188
- const validityChecker = {};
189
- // For each classified issue:
190
- Object.keys(issuesClassification).forEach(issueID => {
191
- const {tools, weight} = issuesClassification[issueID];
192
- // If the weight is invalid:
193
- if (weight < 1 || weight > 4) {
194
- // Report this.
195
- console.log(`ERROR: Issue ${issueID} weight is invalid`);
196
- }
197
- // For each tool that has any rules belonging to the issue:
198
- Object.keys(tools).forEach(toolID => {
199
- // For each such rule:
200
- Object.keys(tools[toolID]).forEach(ruleID => {
201
- // If it is a duplicate:
202
- if (validityChecker[toolID]?.has(ruleID)) {
203
- // Report this.
204
- console.log(`ERROR: Rule ${ruleID} of tool ${toolID} belongs to 2 issues`);
205
- }
206
- // Otherwise, i.e. if it is not a duplicate:
207
- else {
208
- // Add it to the classified rules of the tool.
209
- validityChecker[toolID] ??= new Set();
210
- validityChecker[toolID].add(ruleID);
211
- }
212
- const rule = tools[toolID][ruleID];
213
- // If it is variable:
214
- if (rule.variable) {
215
- variable[toolID] ??= {};
216
- // Add its ID and the issue ID to the variable rule IDs.
217
- variable[toolID][ruleID] = issueID;
218
- }
219
- // Otherwise, i.e. if it is invariant:
220
- else {
221
- invariant[toolID] ??= {};
222
- // Add its ID and the issue ID to the invariant rule IDs.
223
- invariant[toolID][ruleID] = issueID;
224
- }
225
- });
226
- });
227
- });
228
- // Return the data.
229
- return {
230
- invariant,
231
- variable
232
- };
233
- };
234
179
  // Returns a time stamp from a date.
235
180
  const getTimeStamp = exports.getTimeStamp = date => {
236
181
  const timeStamp = date.toISOString().slice(2).replace(/[-:]/g, '').slice(0, 11);
@@ -449,8 +394,6 @@ exports.processRec = async (testType, dirName, what, url, why) => {
449
394
  };
450
395
  // Concurrency lock for the `recs.json` file.
451
396
  const recsLock = exports.recsLock = createLock();
452
- // Variable and invariant rules.
453
- exports.ruleIDs = getRuleIDs();
454
397
  // Updates the test recommendations as a transaction.
455
398
  const updateRecs = exports.updateRecs = (what, url, why) => recsLock(async () => {
456
399
  // Get the data on waiting recommendations.
@@ -525,7 +468,7 @@ const getReport = exports.getReport = async (timeStamp, jobID) => {
525
468
  }
526
469
  };
527
470
  // Adds issue IDs to the standard instances of a report.
528
- exports.annotateReport = async (ruleIDs, timeStamp, jobID) => {
471
+ exports.annotateReport = async (timeStamp, jobID) => {
529
472
  // Get a copy of the report.
530
473
  const report = await getReport(timeStamp, jobID);
531
474
  // If this failed:
@@ -544,7 +487,7 @@ exports.annotateReport = async (ruleIDs, timeStamp, jobID) => {
544
487
  for (const instance of result?.standardResult?.instances ?? []) {
545
488
  const {ruleID} = instance;
546
489
  // Classify its rule.
547
- const issueID = getIssue(ruleIDs, which, ruleID);
490
+ const issueID = getIssue(which, ruleID);
548
491
  // If the rule was classifiable:
549
492
  if (issueID) {
550
493
  // Add the issue ID to the instance.
@@ -617,7 +560,7 @@ exports.getReportData = async (timeStamp, jobID) => {
617
560
  instances.forEach(instance => {
618
561
  const {catalogIndex, issueID} = instance;
619
562
  // If it has a non-ignorable classified issue ID:
620
- if (issueID && issuesClassification[issueID] && issueID !== 'ignorable') {
563
+ if (issueID && issueSpecs[issueID] && issueID !== 'ignorable') {
621
564
  // Ensure that the tool is in the temporary data.
622
565
  reporterIDSet.add(which);
623
566
  // Ensure that the issue is in the temporary data.