@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.
- package/.markdownlint-cli2.jsonc +12 -0
- package/.vscode/settings.json +3 -0
- package/CLAUDE.md +5 -0
- package/README.md +6 -4
- package/ai0BalanceForm/index.html +1 -1
- package/alerts.js +3 -1
- package/api/getReport.js +66 -0
- package/api/listDiagnoses.js +157 -0
- package/api/listIssues.js +225 -0
- package/api/listReports.js +87 -0
- package/api/listViolators.js +164 -0
- package/api/requestFeature.js +56 -0
- package/api/requestRetest.js +102 -0
- package/api/requestTest.js +106 -0
- package/api/routes.js +97 -0
- package/api/schemas.js +297 -0
- package/api/testSuite.js +294 -0
- package/api/util.js +149 -0
- package/diagnoses/index.js +2 -2
- package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
- package/docs/SERVICE.md +4 -1
- package/docs/decisions/001-api-design.md +59 -49
- package/eslint.config.mjs +13 -3
- package/index.html +1 -1
- package/index.js +202 -188
- package/issues/index.js +13 -21
- package/jsconfig.json +8 -0
- package/llms-full.txt +2 -2
- package/llms.txt +16 -11
- package/mcp.js +129 -65
- package/openapi.yaml +1234 -572
- package/package.json +18 -11
- package/reannotate/index.js +26 -15
- package/reannotateForm/index.js +15 -13
- package/redocly.yaml +4 -0
- package/reportHideForm/index.html +1 -1
- package/reportHideForm/index.js +24 -38
- package/reportIssue/index.js +3 -3
- package/reportIssues/index.js +128 -1
- package/reportUnhideForm/index.js +36 -44
- package/reportsExpungeForm/index.js +22 -16
- package/reportsPruneForm/index.js +22 -12
- package/reportsRewindForm/index.js +17 -9
- package/retestRec/index.js +8 -4
- package/retestRecForm/index.js +18 -6
- package/scripts/generate-openapi.js +83 -0
- package/targets/index.js +18 -13
- package/testOrder/index.js +11 -8
- package/tutorial/index.html +12 -12
- package/util.js +581 -539
- package/api/reportList.js +0 -83
- package/api/reportSummary.js +0 -181
- package/reportIssue/api.js +0 -183
- package/reportIssues/util.js +0 -138
- package/researchAgent.js +0 -206
- package/target/api.js +0 -126
- package/testRecForm/api.js +0 -55
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getReportData,
|
|
8
|
+
const {getReportData, makeReportsExtract, reportsPath} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -16,21 +16,31 @@ exports.answer = async (_, search) => {
|
|
|
16
16
|
const searchParams = new URLSearchParams(search);
|
|
17
17
|
const authCode = searchParams?.get('authCode');
|
|
18
18
|
const jobNames = searchParams?.getAll('report');
|
|
19
|
-
// If the form has
|
|
19
|
+
// If the form has displayed itself after a submission and any reports are to be deleted:
|
|
20
20
|
if (jobNames?.length) {
|
|
21
21
|
// If the authorization code is valid:
|
|
22
22
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
try {
|
|
24
|
+
// For each report to be deleted:
|
|
25
|
+
for (const jobName of jobNames) {
|
|
26
|
+
// Delete it.
|
|
27
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
28
|
+
}
|
|
29
|
+
// Update the extract of the available reports.
|
|
30
|
+
await makeReportsExtract();
|
|
31
|
+
}
|
|
32
|
+
// If this failed:
|
|
33
|
+
catch (error) {
|
|
34
|
+
// Return why.
|
|
35
|
+
return {
|
|
36
|
+
status: 'error',
|
|
37
|
+
message: `Deleting superseded reports failed (${error.message})`
|
|
38
|
+
}
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
32
42
|
else {
|
|
33
|
-
//
|
|
43
|
+
// Return this.
|
|
34
44
|
return {
|
|
35
45
|
status: 'error',
|
|
36
46
|
message: 'Invalid authorization code'
|
|
@@ -44,14 +54,14 @@ exports.answer = async (_, search) => {
|
|
|
44
54
|
for (const reportName of reportNames) {
|
|
45
55
|
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
46
56
|
// Get a summary of it.
|
|
47
|
-
const
|
|
48
|
-
const {error, issueCount, preventedToolCount, url} =
|
|
57
|
+
const reportFacts = await getReportData(timeStamp, jobID);
|
|
58
|
+
const {error, issueCount, preventedToolCount, url} = reportFacts;
|
|
49
59
|
// If this failed:
|
|
50
60
|
if (error) {
|
|
51
61
|
// Return why.
|
|
52
62
|
return {
|
|
53
63
|
status: 'error',
|
|
54
|
-
message: error
|
|
64
|
+
message: error.message
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
// Otherwise, i.e. if it succeeded, add the summary to the array.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getReportData,
|
|
8
|
+
const {getReportData, reportsPath} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -20,12 +20,20 @@ exports.answer = async (_, search) => {
|
|
|
20
20
|
if (jobNames?.length) {
|
|
21
21
|
// If the authorization code is valid:
|
|
22
22
|
if (authCode === process.env.AUTH_CODE) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
try {
|
|
24
|
+
// For each report to be deleted:
|
|
25
|
+
for (const jobName of jobNames) {
|
|
26
|
+
// Delete it.
|
|
27
|
+
await fs.unlink(path.join(reportsPath, `${jobName}.json`));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// If this failed:
|
|
31
|
+
catch (error) {
|
|
32
|
+
// Return why.
|
|
33
|
+
return {
|
|
34
|
+
status: 'error',
|
|
35
|
+
message: `Deleting latest superseding reports failed (${error.message})`
|
|
36
|
+
}
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
// Otherwise, i.e. if the authorization code is invalid:
|
|
@@ -43,8 +51,8 @@ exports.answer = async (_, search) => {
|
|
|
43
51
|
for (const reportName of reportNames) {
|
|
44
52
|
const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
|
|
45
53
|
// Get a summary of it.
|
|
46
|
-
const
|
|
47
|
-
const {error, issueCount, preventedToolCount, url} =
|
|
54
|
+
const reportFacts = await getReportData(timeStamp, jobID);
|
|
55
|
+
const {error, issueCount, preventedToolCount, url} = reportFacts;
|
|
48
56
|
// If this failed:
|
|
49
57
|
if (error) {
|
|
50
58
|
// Return why.
|
package/retestRec/index.js
CHANGED
|
@@ -5,16 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {
|
|
8
|
+
const {getLatestReportExtracts, processRec} = require('../util');
|
|
9
9
|
|
|
10
10
|
// FUNCTIONS
|
|
11
11
|
|
|
12
12
|
// Records a retest recommendation and returns an acknowledgement page.
|
|
13
13
|
exports.answer = async (pageArgs, why) => {
|
|
14
14
|
const [timeStamp, jobID] = pageArgs.split('/');
|
|
15
|
-
// Get
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
// Get data on the latest available reports.
|
|
16
|
+
const reportExtracts = await getLatestReportExtracts();
|
|
17
|
+
// Get data on the report whose page is to be retested.
|
|
18
|
+
const reportExtract = reportExtracts.find(
|
|
19
|
+
reportExtract => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
|
|
20
|
+
);
|
|
21
|
+
const {error, url, what} = reportExtract;
|
|
18
22
|
// If this failed:
|
|
19
23
|
if (error) {
|
|
20
24
|
// Return why.
|
package/retestRecForm/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getAgoString, getDateTimeString,
|
|
8
|
+
const {getAgoString, getDateTimeString, getLatestReportExtracts} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -14,14 +14,26 @@ const path = require('path');
|
|
|
14
14
|
// Returns a retest recommendation form.
|
|
15
15
|
exports.answer = async pageArgs => {
|
|
16
16
|
const [timeStamp, jobID] = pageArgs.split('/');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Get data on the latest available reports.
|
|
18
|
+
const reportExtracts = await getLatestReportExtracts();
|
|
19
|
+
// Get data on the report whose page is to be retested.
|
|
20
|
+
const reportExtract = reportExtracts.find(
|
|
21
|
+
reportExtract => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
|
|
22
|
+
);
|
|
23
|
+
// Initialize the page description.
|
|
24
|
+
let target;
|
|
25
|
+
// If getting the data succeeded:
|
|
26
|
+
if (reportExtract) {
|
|
27
|
+
// Update the page description.
|
|
28
|
+
target = reportExtract.what;
|
|
29
|
+
}
|
|
30
|
+
// Otherwise, i.e. if it failed:
|
|
31
|
+
else {
|
|
20
32
|
// Make the form report the failure.
|
|
21
|
-
|
|
33
|
+
target = 'The specified page is not available for retesting';
|
|
22
34
|
}
|
|
23
35
|
const query = {
|
|
24
|
-
target
|
|
36
|
+
target,
|
|
25
37
|
timeStamp,
|
|
26
38
|
jobID,
|
|
27
39
|
ago: getAgoString(timeStamp),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*
|
|
2
|
+
generate-openapi.js
|
|
3
|
+
Generates openapi.yaml from the Kilotest API route table (api/routes.js) and Zod schemas (api/schemas.js), keeping the OpenAPI document in sync with schemas.js’s single source of truth.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const yaml = require('js-yaml');
|
|
11
|
+
const {createDocument} = require('zod-openapi');
|
|
12
|
+
const {license, version} = require('../package.json');
|
|
13
|
+
const routes = require('../api/routes');
|
|
14
|
+
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
|
|
17
|
+
const outputPath = path.join(__dirname, '..', 'openapi.yaml');
|
|
18
|
+
|
|
19
|
+
// FUNCTIONS
|
|
20
|
+
|
|
21
|
+
// Builds the paths object for createDocument from the route table.
|
|
22
|
+
const buildPaths = () => {
|
|
23
|
+
const paths = {};
|
|
24
|
+
routes.forEach(route => {
|
|
25
|
+
const {method, path: routePath, summary, pathParamsSchema, bodySchema, responseSchema} = route;
|
|
26
|
+
if (!paths[routePath]) {
|
|
27
|
+
paths[routePath] = {};
|
|
28
|
+
}
|
|
29
|
+
const operation = {
|
|
30
|
+
operationId: route.operationId,
|
|
31
|
+
summary,
|
|
32
|
+
responses: {
|
|
33
|
+
'200': {
|
|
34
|
+
description: summary,
|
|
35
|
+
content: {
|
|
36
|
+
'application/json': {schema: responseSchema}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (pathParamsSchema) {
|
|
42
|
+
operation.requestParams = {path: pathParamsSchema};
|
|
43
|
+
}
|
|
44
|
+
if (bodySchema) {
|
|
45
|
+
operation.requestBody = {
|
|
46
|
+
content: {'application/json': {schema: bodySchema}}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
paths[routePath][method] = operation;
|
|
50
|
+
});
|
|
51
|
+
return paths;
|
|
52
|
+
};
|
|
53
|
+
// Generates and writes the openapi.yaml file.
|
|
54
|
+
const generate = () => {
|
|
55
|
+
const document = createDocument({
|
|
56
|
+
openapi: '3.1.0',
|
|
57
|
+
info: {
|
|
58
|
+
title: 'Kilotest API',
|
|
59
|
+
description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 10 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.',
|
|
60
|
+
version,
|
|
61
|
+
license: {
|
|
62
|
+
name: license,
|
|
63
|
+
identifier: license
|
|
64
|
+
},
|
|
65
|
+
contact: {
|
|
66
|
+
name: 'Kilotest',
|
|
67
|
+
url: 'https://github.com/jrpool/kilotest',
|
|
68
|
+
email: 'info@kilotest.com'
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
servers: [
|
|
72
|
+
{url: 'https://kilotest.com', description: 'Kilotest production server'}
|
|
73
|
+
],
|
|
74
|
+
security: [],
|
|
75
|
+
paths: buildPaths()
|
|
76
|
+
});
|
|
77
|
+
fs.writeFileSync(outputPath, yaml.dump(document, {noRefs: true, lineWidth: -1}));
|
|
78
|
+
console.log(`Wrote ${outputPath}`);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// EXECUTION
|
|
82
|
+
|
|
83
|
+
generate();
|
package/targets/index.js
CHANGED
|
@@ -9,13 +9,15 @@ const {
|
|
|
9
9
|
getAgoDays,
|
|
10
10
|
getCountString,
|
|
11
11
|
getJobNames,
|
|
12
|
+
getMultiReportWhats,
|
|
12
13
|
getObject,
|
|
13
14
|
getPageDataStrings,
|
|
14
15
|
getRecs,
|
|
15
|
-
getLogs,
|
|
16
16
|
getReportData,
|
|
17
|
+
getReportExtracts,
|
|
17
18
|
isRecommendable,
|
|
18
|
-
jobsPath
|
|
19
|
+
jobsPath,
|
|
20
|
+
objectSort
|
|
19
21
|
} = require('../util');
|
|
20
22
|
const fs = require('fs/promises');
|
|
21
23
|
const path = require('path');
|
|
@@ -69,18 +71,21 @@ const populateQuery = async query => {
|
|
|
69
71
|
query.noQueued = lines.queue.length ? '' : 'No pages are queued for testing.';
|
|
70
72
|
// Add a no-claimed message, if applicable, to the query.
|
|
71
73
|
query.noClaimed = lines.claimed.length ? '' : 'No pages are being tested now.';
|
|
72
|
-
// Get
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
query.
|
|
74
|
+
// Get extracts of all available reports.
|
|
75
|
+
const reportExtracts = await getReportExtracts();
|
|
76
|
+
const reportCount = reportExtracts.length;
|
|
77
|
+
query.which = reportCount ? 'the following' : 'no';
|
|
78
|
+
query.some = (reportCount || jobFileNames.queue.length || jobFileNames.claimed.length)
|
|
76
79
|
? 'another'
|
|
77
80
|
: 'a';
|
|
78
|
-
const
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
const multiReportWhats = await getMultiReportWhats();
|
|
82
|
+
// Sort them primarily by page description and secondarily by completion time.
|
|
83
|
+
let sortedExtracts = objectSort(reportExtracts, 'reportTime', 'alpha');
|
|
84
|
+
sortedExtracts = objectSort(sortedExtracts, 'what', 'alpha');
|
|
85
|
+
// For each report:
|
|
86
|
+
for (const extract of sortedExtracts) {
|
|
87
|
+
const {jobID, timeStamp, url, what} = extract;
|
|
88
|
+
// Get data about it.
|
|
84
89
|
const reportData = await getReportData(timeStamp, jobID);
|
|
85
90
|
const {
|
|
86
91
|
error,
|
|
@@ -104,7 +109,7 @@ const populateQuery = async query => {
|
|
|
104
109
|
const daysAgo = getAgoDays(timeStamp);
|
|
105
110
|
const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {what, url, daysAgo});
|
|
106
111
|
const {urlLink, testInfo} = pageDataStrings;
|
|
107
|
-
const testText =
|
|
112
|
+
const testText = multiReportWhats.includes(what) ? ` (${testInfo.toLowerCase()})` : '';
|
|
108
113
|
lines.tested.push(`${margin} <summary>${what}${testText}</summary>`);
|
|
109
114
|
lines.tested.push(`${margin} <ul>`);
|
|
110
115
|
// Add the URL of the target to the lines.
|
package/testOrder/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {getJSON, getNowStamp, getRandomString, getRecs, isURL} = require('../util');
|
|
8
|
+
const {getJSON, getNowStamp, getRandomString, getRecs, isURL, jobsPath, recsLock} = require('../util');
|
|
9
9
|
const fs = require('fs/promises');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
@@ -33,15 +33,18 @@ exports.answer = async (url, what, authCode) => {
|
|
|
33
33
|
};
|
|
34
34
|
// Save the job in the queue.
|
|
35
35
|
await fs.writeFile(
|
|
36
|
-
path.join(
|
|
36
|
+
path.join(jobsPath, 'queue', `${jobName}.json`), getJSON(job)
|
|
37
37
|
);
|
|
38
38
|
console.log(`Retest queued for ${what} as job ${jobName}`);
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
// Isolate this revision.
|
|
40
|
+
await recsLock(async () => {
|
|
41
|
+
// Get the recommendations.
|
|
42
|
+
const recs = await getRecs();
|
|
43
|
+
// Delete the recommendations to retest the target.
|
|
44
|
+
delete recs[url];
|
|
45
|
+
// Save the revised recommendations.
|
|
46
|
+
await fs.writeFile(path.join(jobsPath, 'recs.json'), getJSON(recs));
|
|
47
|
+
});
|
|
45
48
|
// Get the answer template.
|
|
46
49
|
let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
|
|
47
50
|
// Replace its placeholders.
|
package/tutorial/index.html
CHANGED
|
@@ -141,23 +141,23 @@
|
|
|
141
141
|
}
|
|
142
142
|
@media print {
|
|
143
143
|
/* ── Layout ── */
|
|
144
|
-
body {
|
|
145
|
-
main {
|
|
144
|
+
body {font-size: 11pt;}
|
|
145
|
+
main {max-width: none; padding: 0;}
|
|
146
146
|
|
|
147
147
|
/* ── Hide elements that serve no purpose on a printed page ── */
|
|
148
148
|
.skip-link,
|
|
149
149
|
.tutorial-nav,
|
|
150
150
|
#comment-form,
|
|
151
151
|
#comment-diagnosis,
|
|
152
|
-
#lesson1-quiz button {
|
|
152
|
+
#lesson1-quiz button {display: none;}
|
|
153
153
|
|
|
154
154
|
/* ── Page breaks: start each h2 section, h3 section, and h4 subsection on a new page ── */
|
|
155
|
-
section.tutorial-section {
|
|
156
|
-
section.tutorial-section > section {
|
|
157
|
-
h3, h4 {
|
|
155
|
+
section.tutorial-section {break-before: page;}
|
|
156
|
+
section.tutorial-section > section {break-before: page;}
|
|
157
|
+
h3, h4 {break-before: page;}
|
|
158
158
|
|
|
159
159
|
/* ── Keep headings attached to the content that follows them ── */
|
|
160
|
-
h2, h3, h4 {
|
|
160
|
+
h2, h3, h4 {break-after: avoid;}
|
|
161
161
|
|
|
162
162
|
/* ── Avoid splitting self-contained blocks across pages ── */
|
|
163
163
|
pre,
|
|
@@ -165,15 +165,15 @@
|
|
|
165
165
|
.ai-output,
|
|
166
166
|
.note,
|
|
167
167
|
dl.compare,
|
|
168
|
-
.knowledge-check {
|
|
168
|
+
.knowledge-check {break-inside: avoid;}
|
|
169
169
|
|
|
170
170
|
/* ── Force all details elements open so their content prints ── */
|
|
171
|
-
details {
|
|
172
|
-
details > * {
|
|
173
|
-
details > summary::marker {
|
|
171
|
+
details {display: block;}
|
|
172
|
+
details > * {display: block;}
|
|
173
|
+
details > summary::marker {content: '';}
|
|
174
174
|
|
|
175
175
|
/* ── Print the href after external links as a reference ── */
|
|
176
|
-
a[href^="http"]::after {
|
|
176
|
+
a[href^="http"]::after {content: ' (' attr(href) ')'; font-size: 0.8em;}
|
|
177
177
|
}
|
|
178
178
|
</style>
|
|
179
179
|
</head>
|