@jrpool/kilotest 38.1.0 → 39.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.
Files changed (57) hide show
  1. package/.markdownlint-cli2.jsonc +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/CLAUDE.md +5 -0
  4. package/README.md +6 -4
  5. package/ai0BalanceForm/index.html +1 -1
  6. package/alerts.js +3 -1
  7. package/api/getReport.js +66 -0
  8. package/api/listDiagnoses.js +157 -0
  9. package/api/listIssues.js +225 -0
  10. package/api/listReports.js +87 -0
  11. package/api/listViolators.js +164 -0
  12. package/api/requestFeature.js +56 -0
  13. package/api/requestRetest.js +102 -0
  14. package/api/requestTest.js +106 -0
  15. package/api/routes.js +97 -0
  16. package/api/schemas.js +297 -0
  17. package/api/testSuite.js +294 -0
  18. package/api/util.js +149 -0
  19. package/diagnoses/index.js +2 -2
  20. package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
  21. package/docs/SERVICE.md +4 -1
  22. package/docs/decisions/001-api-design.md +59 -49
  23. package/eslint.config.mjs +13 -3
  24. package/index.html +1 -1
  25. package/index.js +202 -188
  26. package/issues/index.js +13 -21
  27. package/jsconfig.json +8 -0
  28. package/llms-full.txt +2 -2
  29. package/llms.txt +16 -11
  30. package/mcp.js +129 -65
  31. package/openapi.yaml +1234 -572
  32. package/package.json +18 -11
  33. package/reannotate/index.js +26 -15
  34. package/reannotateForm/index.js +15 -13
  35. package/redocly.yaml +4 -0
  36. package/reportHideForm/index.html +1 -1
  37. package/reportHideForm/index.js +24 -38
  38. package/reportIssue/index.js +3 -3
  39. package/reportIssues/index.js +128 -1
  40. package/reportUnhideForm/index.js +36 -44
  41. package/reportsExpungeForm/index.js +22 -16
  42. package/reportsPruneForm/index.js +22 -12
  43. package/reportsRewindForm/index.js +17 -9
  44. package/retestRec/index.js +8 -4
  45. package/retestRecForm/index.js +18 -6
  46. package/scripts/generate-openapi.js +83 -0
  47. package/targets/index.js +18 -13
  48. package/testOrder/index.js +11 -8
  49. package/tutorial/index.html +12 -12
  50. package/util.js +581 -539
  51. package/api/reportList.js +0 -83
  52. package/api/reportSummary.js +0 -181
  53. package/reportIssue/api.js +0 -183
  54. package/reportIssues/util.js +0 -138
  55. package/researchAgent.js +0 -206
  56. package/target/api.js +0 -126
  57. package/testRecForm/api.js +0 -55
package/api/reportList.js DELETED
@@ -1,83 +0,0 @@
1
- /*
2
- api.js
3
- Responds to the reportList API request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {
9
- getAgoDays,
10
- getDateTime,
11
- getLogs,
12
- getNowStamp,
13
- getRandomString,
14
- getReportSize
15
- } = require('../util');
16
-
17
- // CONSTANTS
18
-
19
- const thisHost = process.env.THIS_KILOTEST_HOST;
20
-
21
- // FUNCTIONS
22
-
23
- // Returns a response to an API request for reports.
24
- exports.response = async () => {
25
- // Initialize an array of facts about reports.
26
- const reportsFacts = [];
27
- // Get the available logs, with added job names.
28
- const availableLogs = await getLogs();
29
- // For each log:
30
- for (const availableLog of availableLogs) {
31
- const {jobName} = availableLog;
32
- const [timeStamp, jobID] = jobName.split('-');
33
- const reportSize = await getReportSize(timeStamp, jobID);
34
- // If its report exists:
35
- if (reportSize) {
36
- const {superseded, url, what} = availableLog;
37
- // Add facts about the report to the array.
38
- reportsFacts.push({
39
- identifier: jobName,
40
- 'creation date and time': getDateTime(timeStamp),
41
- 'days since the creation date': getAgoDays(timeStamp),
42
- 'tested web page': {
43
- description: what,
44
- URL: url
45
- },
46
- 'whether a later report about the same page exists': !! superseded,
47
- 'URLs for more details': {
48
- 'for you': `${thisHost}/api/reportSummary/${timeStamp}/${jobID}`,
49
- 'for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
50
- },
51
- 'size of the report in bytes': reportSize,
52
- 'URL to get the entire report as machine-oriented JSON': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`
53
- });
54
- }
55
- // Otherwise, i.e. if the report does not exist:
56
- else {
57
- // Report this.
58
- console.error(`Log ${jobName} exists but its report does not.`);
59
- }
60
- }
61
- // Create a response body.
62
- const content = {
63
- summary: `This document fulfills a request made by a language model to a Kilotest tool. The model asked which reports about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages are available from Kilotest. Each report describes the results of a job that tested one web page. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten rule engines, performs more than a thousand tests on a public web page, using a combination of rule- and machine-learning-based methods. Kilotest exposes API endpoints and web UI URLs to recommend web pages for testing and to obtain information from Kilotest reports. You and your users can learn more about Kilotest and the advantages of testing with an ensemble of rule engines at the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}).`,
64
- 'tool collection name': 'Kilotest',
65
- 'tool name': 'listAllAvailableReports',
66
- request: {
67
- description: 'List all available reports. For each report, the list should identify which page was tested and when and provide URLs to begin incremental retrieval of the test results from the report.',
68
- method: 'GET',
69
- URLs: {
70
- 'for you': `${thisHost}/api/reportList`,
71
- 'for humans': `${thisHost}/targets.html`
72
- },
73
- 'closest ancestor request': null
74
- },
75
- 'response metadata': {
76
- identifier: `${getNowStamp()}-${getRandomString(3)}`,
77
- 'date and time': new Date().toISOString()
78
- },
79
- 'response content': reportsFacts
80
- };
81
- // Return it.
82
- return content;
83
- };
@@ -1,181 +0,0 @@
1
- /*
2
- api.js
3
- Responds to the reportSummary API request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {
9
- getNowStamp,
10
- getRandomString,
11
- getReport,
12
- isHidden,
13
- objectSort,
14
- ruleEngines
15
- } = require('../util');
16
- const issuesClassification = require('testilo/procs/score/tic').issues;
17
-
18
- // CONSTANTS
19
-
20
- const thisHost = process.env.THIS_KILOTEST_HOST;
21
-
22
- // FUNCTIONS
23
-
24
- // Returns facts about a report.
25
- const getReportFacts = async (report) => {
26
- // Initialize the facts.
27
- const facts = {
28
- ruleEngineIDs: new Set(),
29
- reporterIDs: new Set(),
30
- issueIDs: new Set(),
31
- violators: new Set()
32
- };
33
- // For each act in the report:
34
- report.acts.forEach(act => {
35
- // If it is a test act:
36
- if (act.type === 'test') {
37
- const {result, which} = act;
38
- // Ensure its rule engine is in the facts.
39
- facts.ruleEngineIDs.add(which);
40
- const instances = result?.standardResult?.instances ?? [];
41
- // For each of its standard instances:
42
- instances.forEach(instance => {
43
- const {catalogIndex, issueID} = instance;
44
- // If the instance identifies a non-ignorable issue:
45
- if (issueID && issueID !== 'ignorable') {
46
- const issueClassification = issuesClassification[issueID];
47
- // If the issue is classified and has a valid weight:
48
- if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
49
- // Ensure the issue ID is in the facts.
50
- facts.issueIDs.add(issueID);
51
- // Ensure its rule engine is among the reporters in the facts.
52
- facts.reporterIDs.add(which);
53
- // If the instance has a catalog index:
54
- if (catalogIndex) {
55
- // Ensure the violator is in the facts.
56
- facts.violators.add(catalogIndex);
57
- }
58
- }
59
- }
60
- });
61
- }
62
- });
63
- // Return the facts.
64
- return facts;
65
- };
66
- // Returns facts about a rule engine.
67
- const getRuleEngineFacts = ruleEngineID => {
68
- const ruleEngineData = ruleEngines[ruleEngineID];
69
- return {
70
- identifier: ruleEngineID,
71
- name: ruleEngineData[0],
72
- sponsor: ruleEngineData[1]
73
- };
74
- };
75
- // Returns facts about rule engines that were prevented from testing the page.
76
- const getPreventionFacts = report => {
77
- return objectSort(Object.entries(report.jobData.preventions).map(([ruleEngineID, reason]) => ({
78
- 'name': getRuleEngineFacts(ruleEngineID).name,
79
- 'reason for failure': reason
80
- })), 'name', 'alpha');
81
- };
82
- // Returns rule engine IDs sorted by name.
83
- const getSortedRuleEngineIDs = ruleEngineIDSet => {
84
- return Array.from(ruleEngineIDSet).sort((a, b) => {
85
- const aData = ruleEngines[a][0];
86
- const bData = ruleEngines[b][0];
87
- return aData.localeCompare(bData, 'en', {sensitivity: 'base'});
88
- });
89
- };
90
- // Returns issue IDs sorted by priority and summary.
91
- const getSortedIssueIDs = issueIDSet => {
92
- return Array.from(issueIDSet).sort((a, b) => {
93
- const aData = issuesClassification[a];
94
- const bData = issuesClassification[b];
95
- if (aData.weight !== bData.weight) {
96
- return bData.weight - aData.weight;
97
- }
98
- return aData.summary.localeCompare(bData.summary, 'en', {sensitivity: 'base'});
99
- });
100
- };
101
- // Get facts about an issue.
102
- const getIssueFacts = (issueID, timeStamp, jobID) => {
103
- const issueData = issuesClassification[issueID];
104
- const {summary, weight, why} = issueData;
105
- return {
106
- identifier: issueID,
107
- summary,
108
- 'impact on a user': why,
109
- 'priority': ['lowest', 'low', 'high', 'highest'][weight - 1],
110
- 'URLs for more details': {
111
- 'for you': `${thisHost}/api/reportIssue/${issueID}/${timeStamp}/${jobID}`,
112
- 'for humans': `${thisHost}/reportIssue.html/${issueID}/${timeStamp}/${jobID}`
113
- }
114
- };
115
- };
116
- // Returns a response to an API request for a summary of one report.
117
- exports.response = async (args) => {
118
- const [timeStamp, jobID] = args;
119
- const reportIsHidden = await isHidden(timeStamp, jobID);
120
- // If the report exists and is hidden:
121
- if (reportIsHidden) {
122
- // Return this.
123
- return {
124
- status: 'error',
125
- message: 'Report not available',
126
- };
127
- }
128
- // Otherwise, i.e. if the report is not hidden, get it.
129
- const report = await getReport(timeStamp, jobID);
130
- const {error} = report;
131
- // If this failed:
132
- if (error) {
133
- // Return why.
134
- return {
135
- status: 'error',
136
- message: error
137
- };
138
- }
139
- // Otherwise, i.e. if it succeeded, get facts about it.
140
- const reportFacts = await getReportFacts(report);
141
- // Create a response body.
142
- const content = {
143
- summary: 'This document fulfills a request made by a language model to a Kilotest tool. The model asked for a summary of one Kilotest report. The model had previously used the listAllAvailableReports tool and had acquired from that tool basic facts about Kilotest, the ensemble testing that Kilotest performs, and the reports available from Kilotest. This summary does not repeat those facts.',
144
- 'tool collection name': 'Kilotest',
145
- 'tool name': 'summarizeOneReport',
146
- request: {
147
- description: 'Summarize one report. The summary should briefly describe the testing job and the results, including the rule engines that tested the web page and the issues that were revealed by the reported rule violations, and provide URLs for getting details about issues. The timeStamp and jobID parameters identify the report and were obtained from the response to a listAllAvailableReports operation.',
148
- method: 'GET',
149
- URLs: {
150
- 'for you': `${thisHost}/api/reportSummary/${timeStamp}/${jobID}`,
151
- 'for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
152
- },
153
- 'closest ancestor request': {
154
- 'tool name': 'listAllAvailableReports',
155
- description: 'List all available reports.',
156
- URLs: {
157
- 'for you': `${thisHost}/api/reportList`,
158
- 'for humans': `${thisHost}/targets.html`
159
- }
160
- }
161
- },
162
- 'response metadata': {
163
- identifier: `${getNowStamp()}-${getRandomString(3)}`,
164
- 'date and time': new Date().toISOString()
165
- },
166
- 'response content': {
167
- 'rule engines that tried to test the page': getSortedRuleEngineIDs(reportFacts.ruleEngineIDs)
168
- .map(id => getRuleEngineFacts(id)),
169
- 'rule engines that could not test the page': getPreventionFacts(report),
170
- 'names of rule engines that reported rule violations': Array
171
- .from(reportFacts.reporterIDs)
172
- .map(id => getRuleEngineFacts(id).name)
173
- .sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'})),
174
- 'number of elements reported as violators': reportFacts.violators.size,
175
- 'issues revealed': getSortedIssueIDs(reportFacts.issueIDs)
176
- .map(id => getIssueFacts(id, timeStamp, jobID))
177
- }
178
- };
179
- // Return it.
180
- return content;
181
- };
@@ -1,183 +0,0 @@
1
- /*
2
- api.js
3
- Responds to the report-issue request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getData} = require('../reportIssues/util');
9
- const {
10
- getDateTime,
11
- getNowStamp,
12
- getRandomString,
13
- getReport,
14
- getToolsFacts,
15
- isHidden,
16
- tools
17
- } = require('../util');
18
-
19
- // FUNCTIONS
20
-
21
- // Gets facts about an issue.
22
- const getIssueFacts = async (timeStamp, jobID, issue) => {
23
- const {issueID, reporterCount, reporters, summary, violatorCount, wcag, why} = issue;
24
- const wcagType = wcag.length === 3 ? 'guideline' : 'success criterion';
25
- // Get the report.
26
- const report = await getReport(timeStamp, jobID);
27
- const {acts, catalog, error} = report;
28
- // If this failed:
29
- if (error) {
30
- // Return why.
31
- return {
32
- status: 'error',
33
- message: error
34
- };
35
- }
36
- const violatorIndexSet = new Set();
37
- // For each act of the report:
38
- acts.forEach(act => {
39
- // If it is a test act:
40
- if (act.type === 'test') {
41
- const {result} = act;
42
- const instances = result?.standardResult?.instances ?? [];
43
- // For each standard instance of the act:
44
- instances.forEach(instance => {
45
- const {catalogIndex} = instance;
46
- // If its issue is the issue to be described and the instance has a catalog index:
47
- if (instance.issueID === issueID && catalogIndex) {
48
- // Ensure that the violator is in the temporary data.
49
- violatorIndexSet.add(catalogIndex);
50
- }
51
- });
52
- }
53
- });
54
- return {
55
- identifier: issueID,
56
- summary,
57
- 'related WCAG 2.2 standard': {
58
- layer: wcagType,
59
- 'numeric identifier': wcag
60
- },
61
- 'impact on a user': why,
62
- 'rule engines reporting the issue': {
63
- 'number': reporterCount,
64
- 'names': reporters.map(tool => tool.toolName)
65
- },
66
- 'number of HTML elements reported as exhibiting the issue': violatorCount,
67
- 'HTML elements reported as exhibiting the issue': Array
68
- .from(violatorIndexSet)
69
- .filter(index => catalog[index])
70
- .map(index => ({
71
- identifier: String(index),
72
- 'tag name': catalog[index].tagName,
73
- 'id attribute': catalog[index].id,
74
- 'start tag': catalog[index].startTag,
75
- 'inner text': catalog[index].text,
76
- 'inner text usable as a text fragment for linking': catalog[index].textLinkable,
77
- 'x, y, width, height in pixels': catalog[index].boxID.split(':'),
78
- 'XPath': catalog[index].pathID
79
- }))
80
- };
81
- };
82
- // Returns a response to a report-issue request.
83
- exports.response = async args => {
84
- const [issueID, timeStamp, jobID] = args;
85
- const reportIsHidden = await isHidden(timeStamp, jobID);
86
- // If the report is not available:
87
- if (reportIsHidden) {
88
- // Return this.
89
- return {
90
- status: 'error',
91
- message: 'Report not available'
92
- };
93
- }
94
- // Otherwise, i.e. if the report is available, get data on the report and its issues.
95
- const data = await getData(timeStamp, jobID);
96
- const {error, issuesData, pageData} = data;
97
- // If this failed:
98
- if (error) {
99
- // Return why.
100
- return {
101
- status: 'error',
102
- message: error
103
- };
104
- }
105
- const {what, url, daysAgo} = pageData;
106
- const {issueCount, issues, preventions, reporterCount, reporters, violatorCount} = issuesData;
107
- let issue;
108
- // Otherwise, i.e. if it succeeded, get the level of and the data on the issue.
109
- const issueLevel = [4, 3, 2, 1].find(level => {
110
- issue = issues[level].find(issue => issue.issueID === issueID);
111
- return issue;
112
- });
113
- // If the issue was not one of those in the report:
114
- if (! issue) {
115
- // Return this.
116
- return {
117
- status: 'error',
118
- message: 'Issue not found in the report'
119
- };
120
- }
121
- const preventedTools = Object.entries(preventions).map(prevention => ({
122
- name: tools[prevention[0]][0],
123
- 'reason for failure': prevention[1]
124
- }));
125
- const thisHost = process.env.THIS_KILOTEST_HOST;
126
- // Get a response.
127
- const content = {
128
- summary: `This document fulfills a request made by a language model to a Kilotest tool. The model requested data, drawn from a Kilotest report, about one of the issues for the front-end quality (i.e. accessibility, usability, and standards conformity) of a web page. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten rule engines, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes several API endpoints to recommend web pages for testing and to obtain information from Kilotest reports. To learn more about Kilotest and the advantages of testing with an ensemble of rule engines, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), which contains an introduction on its home page and a tutorial.`,
129
- 'tool collection name': 'Kilotest',
130
- 'tool name': 'describeOneIssueFromOneReport',
131
- request: {
132
- 'type of request': {
133
- identifier: 'reportIssue',
134
- description: 'Describe one issue from one report.'
135
- },
136
- method: 'GET',
137
- URLs: {
138
- 'for you': `${thisHost}/api/reportIssue/${issueID}/${timeStamp}/${jobID}`,
139
- 'for humans': `${thisHost}/reportIssue.html/${issueID}/${timeStamp}/${jobID}`
140
- },
141
- 'closest ancestor request': {
142
- identifier: 'summarizeOneReport',
143
- description: 'Summarize one report.',
144
- URLs: {
145
- 'for you': `${thisHost}/api/reportSummary/${timeStamp}/${jobID}`,
146
- 'for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
147
- }
148
- }
149
- },
150
- 'response metadata': {
151
- identifier: `${getNowStamp()}-${getRandomString(3)}`,
152
- 'date and time': new Date().toISOString()
153
- },
154
- report: {
155
- identifier: `${timeStamp}-${jobID}`,
156
- 'creation date': getDateTime(timeStamp),
157
- 'days since the creation date': daysAgo
158
- },
159
- 'tested web page': {
160
- description: what,
161
- URL: url
162
- },
163
- 'rule engines that tried to test the page': getToolsFacts(Object.keys(tools)),
164
- 'rule engines that were unable to test the page': preventedTools,
165
- 'rule engines that reported issues': {
166
- number: reporterCount,
167
- names: reporters.map(tool => tool.toolName)
168
- },
169
- 'number of issues reported': {
170
- total: issueCount,
171
- 'by priority': {
172
- 'highest priority': issues[4].length,
173
- 'high priority': issues[3].length,
174
- 'low priority': issues[2].length,
175
- 'lowest priority': issues[1].length
176
- }
177
- },
178
- 'number of HTML elements reported as exhibiting issues': violatorCount,
179
- 'level of the issue': issueLevel,
180
- 'facts about the issue': await getIssueFacts(timeStamp, jobID, issue)
181
- };
182
- return content;
183
- };
@@ -1,138 +0,0 @@
1
- /*
2
- util.js
3
- Shared utilities.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {
9
- getPageData, getReport, getToolsData, getToolList, isHidden, isValidReport, objectSort
10
- } = require('../util');
11
- const issuesClassification = require('testilo/procs/score/tic').issues;
12
-
13
- // FUNCTIONS
14
-
15
- // Returns data on the issues reported by a report.
16
- const getIssuesData = async (timeStamp, jobID) => {
17
- // Get the report.
18
- const report = await getReport(timeStamp, jobID);
19
- const reportIsHidden = await isHidden(timeStamp, jobID);
20
- // If it exists and is hidden:
21
- if (reportIsHidden) {
22
- return {error: 'Report is not available'}
23
- }
24
- // Otherwise, if it exists and is valid:
25
- if (isValidReport(report)) {
26
- // Initialize the temporary data.
27
- const temp = {
28
- issues: {},
29
- reporters: new Set(),
30
- violators: new Set()
31
- };
32
- // Initialize the final data.
33
- const final = {
34
- reporters: [],
35
- reporterList: '',
36
- reporterCount: 0,
37
- violatorCount: 0,
38
- preventions: report.jobData.preventions,
39
- issues: {
40
- 4: [],
41
- 3: [],
42
- 2: [],
43
- 1: []
44
- },
45
- issueCount: 0
46
- };
47
- // For each act in the report:
48
- report.acts.forEach(act => {
49
- // If it is a test act:
50
- if (act.type === 'test') {
51
- const {result, which} = act;
52
- const instances = result?.standardResult?.instances ?? [];
53
- // For each of its standard instances:
54
- instances.forEach(instance => {
55
- const {catalogIndex, issueID} = instance;
56
- // If the instance identifies its rule as belonging to a non-ignorable issue:
57
- if (issueID && issueID !== 'ignorable') {
58
- const issueClassification = issuesClassification[issueID];
59
- // If the issue has a current weighted classification:
60
- if (issueClassification && [1, 2, 3, 4].includes(issueClassification.weight)) {
61
- const {summary, wcag, weight, why} = issueClassification;
62
- // Initialize the temporary data on the issue if necessary.
63
- temp.issues[issueID] ??= {
64
- issueID,
65
- summary,
66
- wcag,
67
- why,
68
- weight,
69
- reporters: new Set(),
70
- reporterList: '',
71
- violators: new Set()
72
- };
73
- // Ensure the tool is in the temporary data.
74
- temp.issues[issueID].reporters.add(which);
75
- temp.reporters.add(which);
76
- // If the instance has a catalog index:
77
- if (catalogIndex) {
78
- // Ensure the violator is in the temporary data.
79
- temp.issues[issueID].violators.add(catalogIndex);
80
- temp.violators.add(catalogIndex);
81
- }
82
- }
83
- }
84
- });
85
- }
86
- });
87
- // Finish populating the final data.
88
- final.reporters = getToolsData(temp.reporters);
89
- final.reporterList = getToolList(temp.reporters);
90
- final.reporterCount = final.reporters.length;
91
- final.violatorCount = temp.violators.size;
92
- Object.values(temp.issues).forEach(issue => {
93
- const {issueID, summary, wcag, why, weight} = issue;
94
- const finalIssue = {
95
- issueID,
96
- summary,
97
- wcag,
98
- why,
99
- weight
100
- };
101
- finalIssue.reporters = getToolsData(issue.reporters);
102
- finalIssue.reporterList = getToolList(issue.reporters);
103
- finalIssue.reporterCount = finalIssue.reporters.length;
104
- finalIssue.violatorCount = issue.violators.size;
105
- final.issues[issue.weight].push(finalIssue);
106
- });
107
- final.issueCount = Object.keys(temp.issues).length;
108
- // For each weight:
109
- [4, 3, 2, 1].forEach(weight => {
110
- // Sort its issues in the final data alphabetically by reporter names.
111
- objectSort(final.issues[weight], 'reporterList', 'alpha');
112
- // Sort the issues again in descending reporter-count order, making this the primary order.
113
- objectSort(final.issues[weight], 'reporterCount', 'numericDown');
114
- });
115
- // Return the data.
116
- return final;
117
- }
118
- // Otherwise, i.e. if it is invalid or does not exist, return this.
119
- return {error: 'Report missing or invalid.'};
120
- };
121
- // Get page and issues data from a report.
122
- exports.getData = async (timeStamp, jobID) => {
123
- const pageData = await getPageData(timeStamp, jobID);
124
- const issuesData = await getIssuesData(timeStamp, jobID);
125
- const pageError = pageData.error || '';
126
- const issuesError = issuesData.error || '';
127
- const errors = [pageError, issuesError].filter(Boolean).join('; ');
128
- // If the data of either type are missing or invalid:
129
- if (errors) {
130
- // Return this.
131
- return {error: errors};
132
- }
133
- // Otherwise, return the data.
134
- return {
135
- pageData,
136
- issuesData
137
- };
138
- };