@jrpool/kilotest 60.9.1 → 61.0.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.
@@ -31,7 +31,7 @@ after(() => {
31
31
  test('listPages returns one entry per tested page, sorted by page description', async () => {
32
32
  const body = await response();
33
33
  const pagesBasics = body['response content']['basics about all tested pages'];
34
- const descriptions = pagesBasics.map(b => b['tested web page'].description);
34
+ const descriptions = pagesBasics.map(b => b.description);
35
35
  assert.deepEqual(
36
36
  descriptions,
37
37
  [
@@ -49,31 +49,32 @@ test('listPages returns one entry per tested page, sorted by page description',
49
49
  test('listPages does not include the page of the hidden report', async () => {
50
50
  const body = await response();
51
51
  const pagesBasics = body['response content']['basics about all tested pages'];
52
- const ids = pagesBasics.map(b => b['basics about the latest report'].identifier);
52
+ const ids = pagesBasics.map(b => b.identifier);
53
53
  assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listPages');
54
54
  });
55
55
 
56
- test('listPages describes a multi-report page by its latest report and its report count', async () => {
56
+ test('listPages identifies a multi-report page by its latest report', async () => {
57
57
  const body = await response();
58
58
  const pagesBasics = body['response content']['basics about all tested pages'];
59
- const mixed = pagesBasics.find(b => b['tested web page'].description === 'Mixed Outcomes')!;
60
- assert.equal(mixed['number of reports about the page'], 2);
61
- const latest = mixed['basics about the latest report'];
62
- assert.equal(latest.identifier, '260202T0000-new');
63
- assert.equal(latest['completion date and time'], '2026-02-02T00:10:00.000Z');
64
- assert.equal(typeof latest['days since the report was completed'], 'number');
65
- assert.equal(mixed['how to get details about the latest report'].method, 'GET');
66
- assert.ok(mixed['how to get details about the latest report'].URL.endsWith('/api/listIssues/260202T0000-new'));
67
- assert.ok(mixed['web users can get details about the latest report at'].endsWith('/listIssues.html/260202T0000-new'));
59
+ const mixed = pagesBasics.find(b => b.description === 'Mixed Outcomes')!;
60
+ assert.equal(mixed.identifier, '260202T0000-new');
61
+ assert.equal(mixed.URL, 'https://example.com/mixed');
68
62
  });
69
63
 
70
64
  test('listPages describes a single-report page', async () => {
71
65
  const body = await response();
72
66
  const pagesBasics = body['response content']['basics about all tested pages'];
73
- const canttell = pagesBasics.find(b => b['tested web page'].description === 'All CantTell Page')!;
74
- assert.equal(canttell['number of reports about the page'], 1);
75
- assert.equal(canttell['tested web page'].URL, 'https://example.com/canttell');
76
- assert.equal(canttell['basics about the latest report'].identifier, '260101T0001-ct');
67
+ const canttell = pagesBasics.find(b => b.description === 'All CantTell Page')!;
68
+ assert.equal(canttell.URL, 'https://example.com/canttell');
69
+ assert.equal(canttell.identifier, '260101T0001-ct');
70
+ });
71
+
72
+ test('listPages includes result-retrieval instructions with an identifier placeholder', async () => {
73
+ const body = await response();
74
+ const content = body['response content'];
75
+ assert.equal(content['how to get test results about a page'].method, 'GET');
76
+ assert.ok(content['how to get test results about a page'].URL.endsWith('/api/listIssues/{identifier}'));
77
+ assert.ok(content['web users can get test results about a page at'].endsWith('/listIssues.html/{identifier}'));
77
78
  });
78
79
 
79
80
  test('listPages includes request-test instructions', async () => {
package/api/listPages.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {z} from 'zod';
9
9
  import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
10
- import {getAgoDays, getReportExtracts} from '../util.ts';
10
+ import {getReportExtracts} from '../util.ts';
11
11
  import {listPagesResponseSchema} from './schemas.ts';
12
12
 
13
13
  // TYPES
@@ -20,42 +20,20 @@ type ResponseContent = z.infer<typeof listPagesResponseSchema>['response content
20
20
  // Returns the response body.
21
21
  export const response = async () => {
22
22
  const thisHost = getThisHost();
23
- // Get extracts of all available reports, sorted by page description and completion time.
24
- const reportExtracts = await getReportExtracts();
25
- // Get the number of reports about each page.
26
- const reportCounts = new Map<string, number>();
27
- reportExtracts.forEach(({description}) => {
28
- reportCounts.set(description, (reportCounts.get(description) ?? 0) + 1);
29
- });
30
- // Initialize an array of basics about the pages.
31
- const pagesBasics: ResponseContent['basics about all tested pages'] = [];
32
- // For each report:
33
- reportExtracts.forEach(extract => {
34
- const {description, id, reportTime, superseded, url} = extract;
35
- // If it is the latest report about its page:
36
- if (!superseded) {
37
- // Add basics about the page, with instructions for getting details, to the array.
38
- pagesBasics.push({
39
- 'tested web page': {
40
- description,
41
- URL: url
42
- },
43
- 'number of reports about the page': reportCounts.get(description)!,
44
- 'basics about the latest report': {
45
- identifier: id,
46
- 'completion date and time': reportTime,
47
- 'days since the report was completed': getAgoDays(new Date(reportTime))
48
- },
49
- 'how to get details about the latest report': {
50
- method: 'GET',
51
- URL: `${thisHost}/api/listIssues/${id}`
52
- },
53
- 'web users can get details about the latest report at': `${thisHost}/listIssues.html/${id}`
54
- });
55
- }
56
- });
23
+ // Get extracts of the latest reports about all tested pages, sorted by page description.
24
+ const reportExtracts = await getReportExtracts(true);
25
+ // Get the basics about each tested page: its description, URL, and the identifier of
26
+ // its latest report, which the result-retrieval instructions below use as a placeholder.
27
+ const pagesBasics: ResponseContent['basics about all tested pages'] = reportExtracts.map(
28
+ ({description, id, url}) => ({description, URL: url, identifier: id})
29
+ );
57
30
  // Create the response content.
58
31
  const responseContent: ResponseContent = {
32
+ 'how to get test results about a page': {
33
+ method: 'GET',
34
+ URL: `${thisHost}/api/listIssues/{identifier}`
35
+ },
36
+ 'web users can get test results about a page at': `${thisHost}/listIssues.html/{identifier}`,
59
37
  'basics about all tested pages': pagesBasics,
60
38
  'how to request that a page with no report be tested': {
61
39
  method: 'POST',
package/api/schemas.ts CHANGED
@@ -195,12 +195,15 @@ const reportTimeSchema = z.object({
195
195
  });
196
196
 
197
197
  export const listPagesResponseSchema = envelope('GET', z.object({
198
+ 'how to get test results about a page': z.object({
199
+ method: z.literal('GET'),
200
+ URL: z.string().describe('URL template; replace {identifier} with the identifier of the latest report about the page.')
201
+ }),
202
+ 'web users can get test results about a page at': z.string().describe('URL template; replace {identifier} with the identifier of the latest report about the page.'),
198
203
  'basics about all tested pages': z.array(z.object({
199
- 'tested web page': z.object({description: z.string(), URL: z.string()}),
200
- 'number of reports about the page': z.number(),
201
- 'basics about the latest report': reportTimeSchema,
202
- 'how to get details about the latest report': z.object({method: z.literal('GET'), URL: z.string()}),
203
- 'web users can get details about the latest report at': z.string()
204
+ description: z.string(),
205
+ URL: z.string(),
206
+ identifier: z.string().describe('Identifier of the latest report about the page (example: 260503T0432-x9z).')
204
207
  })),
205
208
  'how to request that a page with no report be tested': z.object({
206
209
  method: z.literal('POST'),
package/api/util.ts CHANGED
@@ -51,18 +51,20 @@ export const getResponseMetadata = () => ({
51
51
  export const getToolsFacts = () => ({
52
52
  'name': 'Kilotest',
53
53
  'description': {
54
- 'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the tested pages, the issues, and the violations retrievable at four levels of granularity.',
54
+ 'what Kilotest does': 'Kilotest tools generate findings about the front-end quality (i.e. accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG of W3C) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues and then saves the report for public retrieval. Kilotest makes facts about the tested pages, the issues, and the violations retrievable at four levels of granularity.',
55
55
  'how to retrieve findings': {
56
- 'level 1': 'Use the listPages tool to get a list of tested pages, each with its latest report. The listIssues output about any report lists all reports about the same page.',
57
- 'level 2': 'Use the listIssues tool to get a list of issues in one report.',
58
- 'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue.',
56
+ 'level 1': 'Use the listPages tool to get a list of tested pages, each with the identifier of its latest report, for use with the listIssues tool.',
57
+ 'level 2': 'Use the listIssues tool to get a list of issues in one report, each with a link to the level-3 results for that issue. The response also includes a history of reports about the page, each with a link to its own level-2 results.',
58
+ 'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue, each with a link to the level-4 results for that violator.',
59
59
  'level 4': 'Use the listDiagnoses tool to get a list of diagnoses of how one element on one page exhibited one issue in one report.',
60
60
  },
61
61
  'how to generate more findings': {
62
- 'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.',
63
- 'retesting': 'If the listIssues tool shows that the latest report about a page is obsolete, because the page has been revised or for another reason, use the requestRetest tool to request that the page be retested.',
64
- 'latency': 'Requests for testing and retesting are usually approved and fulfilled within one day.',
65
- 'confirmation': 'Use the listPages tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
62
+ 'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested, or the orderNewTest tool for immediate testing.',
63
+ 'retesting': 'If you believe that the latest report about a page is obsolete (e.g., because the page has been revised), use the requestRetest tool to request that the page be retested, or the orderRetest tool for immediate retesting.',
64
+ 'approval': 'Requests are manager-reviewed, usually in 1 hour to one day. Orders are automatically reviewed. Reviews result in approval or rejection.',
65
+ 'latency': 'An approved test typically finishes in 2 to 4 minutes.',
66
+ 'request confirmation': 'After submitting a request and waiting for manager review, you can use the listPages tool to determine whether the request has been fulfilled: a new-test request adds a page entry linking to its results, and a retest request adds a later report about the page.',
67
+ 'order confirmation': 'After ordering a test or retest, you will receive a rejection or a report identifier, which you can use to call the awaitTest tool. That tool blocks until the test completes, fails, or a maximum wait time elapses, and then returns the outcome. If you do not call awaitTest, you can wait 2 to 4 minutes and then, if the test is complete, the listIssues tool called with the report identifier will succeed.'
66
68
  }
67
69
  },
68
70
  'URL': `${getThisHost()}/mcp`,
package/index.test.ts CHANGED
@@ -981,12 +981,12 @@ test('GET /qai/comments redirects to /tutorialAI.html', async () => {
981
981
  assert.equal(res.headers['location'], '/tutorialAI.html');
982
982
  });
983
983
 
984
- test('GET /diagnoses.html/<segments> redirects to the equivalent /listDiagnoses.html/<segments>, preserving the query string', async () => {
984
+ test('GET /diagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> with a query string redirects in one step to the canonical /listDiagnoses.html URL', async () => {
985
985
  const res = await request(
986
- 'GET', '/diagnoses.html/duplicateID/260426T1741-77w/718?pathID=/html/body', {}, {followRedirects: false}
986
+ 'GET', '/diagnoses.html/linkNoText/260101T0000/mix/0?pathID=/html/body', {}, {followRedirects: false}
987
987
  );
988
988
  assert.equal(res.statusCode, 301);
989
- assert.equal(res.headers['location'], '/listDiagnoses.html/duplicateID/260426T1741-77w/718?pathID=/html/body');
989
+ assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
990
990
  });
991
991
 
992
992
  test('GET /reportIssues.html/<reportID> redirects to the equivalent /listIssues.html/<reportID>', async () => {
@@ -1015,12 +1015,12 @@ test('GET /listViolators.html/<issueID>/<timeStamp>/<jobID> redirects to the sin
1015
1015
  assert.equal(res.headers['location'], '/listViolators.html/linkNoText/260101T0000-mix');
1016
1016
  });
1017
1017
 
1018
- test('GET /listDiagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> redirects to the single-reportID URL, preserving the query string', async () => {
1018
+ test('GET /listDiagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> with a query string redirects in one step to the canonical single-reportID URL', async () => {
1019
1019
  const res = await request(
1020
1020
  'GET', '/listDiagnoses.html/linkNoText/260101T0000/mix/0?pathID=/html/body', {}, {followRedirects: false}
1021
1021
  );
1022
1022
  assert.equal(res.statusCode, 301);
1023
- assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0?pathID=/html/body');
1023
+ assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
1024
1024
  });
1025
1025
 
1026
1026
  test('GET /requestRetestForm.html/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
@@ -1077,6 +1077,33 @@ test('GET /api/listReports redirects to /api/listPages', async () => {
1077
1077
  assert.equal(res.headers['location'], '/api/listPages');
1078
1078
  });
1079
1079
 
1080
+ // TESTS: query-string canonicalization
1081
+ // No GET page handler reads the query string, so a request with a query returns a
1082
+ // duplicate of the queryless page. These verify that such requests permanently
1083
+ // redirect to the queryless URL.
1084
+
1085
+ test('GET /listDiagnoses.html/<segments> with a query string redirects to the queryless URL', async () => {
1086
+ const res = await request(
1087
+ 'GET', '/listDiagnoses.html/linkNoText/260101T0000-mix/0?pathID=/html/body', {}, {followRedirects: false}
1088
+ );
1089
+ assert.equal(res.statusCode, 301);
1090
+ assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
1091
+ });
1092
+
1093
+ test('GET /listIssues.html/<reportID> with a query string redirects to the queryless URL', async () => {
1094
+ const res = await request(
1095
+ 'GET', '/listIssues.html/260101T0000-mix?foo=bar', {}, {followRedirects: false}
1096
+ );
1097
+ assert.equal(res.statusCode, 301);
1098
+ assert.equal(res.headers['location'], '/listIssues.html/260101T0000-mix');
1099
+ });
1100
+
1101
+ test('GET / with a query string redirects to /', async () => {
1102
+ const res = await request('GET', '/?utm_source=example', {}, {followRedirects: false});
1103
+ assert.equal(res.statusCode, 301);
1104
+ assert.equal(res.headers['location'], '/');
1105
+ });
1106
+
1080
1107
  test('GET /requestTest.html redirects to /requestNewTest.html', async () => {
1081
1108
  const res = await request('GET', '/requestTest.html', {}, {followRedirects: false});
1082
1109
  assert.equal(res.statusCode, 301);
@@ -1647,12 +1674,15 @@ test('POST /metrics.html with a valid authCode sets a metrics-exclusion cookie',
1647
1674
  assert.ok(setCookie.includes('HttpOnly'));
1648
1675
  });
1649
1676
 
1650
- test('GET /metrics.html with a valid authCode in the query string does not set a cookie or show the counts', async () => {
1677
+ test('GET /metrics.html with a valid authCode in the query string redirects to the queryless URL, which does not set a cookie or show the counts', async () => {
1651
1678
  const res = await request('GET', '/metrics.html?authCode=test-auth-code');
1652
- assert.equal(res.statusCode, 200);
1653
- assert.equal(res.headers['set-cookie'], undefined);
1654
- assert.ok(res.body.includes('Authorization code'));
1655
- assert.ok(!res.body.includes('Web page views'));
1679
+ assert.equal(res.statusCode, 301);
1680
+ assert.equal(res.headers['location'], '/metrics.html');
1681
+ const redirected = await request('GET', res.headers['location']);
1682
+ assert.equal(redirected.statusCode, 200);
1683
+ assert.equal(redirected.headers['set-cookie'], undefined);
1684
+ assert.ok(redirected.body.includes('Authorization code'));
1685
+ assert.ok(!redirected.body.includes('Web page views'));
1656
1686
  });
1657
1687
 
1658
1688
  test('a request carrying the metrics-exclusion cookie does not record a pageViews or apiOperations metric', async () => {
package/index.ts CHANGED
@@ -347,9 +347,10 @@ export const isPathAllowed = (method: string, pathname: string): boolean => {
347
347
  const patterns = (routes as Record<string, string[]>)[method] || [];
348
348
  return patterns.some(pattern => matchPath(pattern, pathname));
349
349
  };
350
- // Returns the single-reportID URL that a legacy two-segment report URL (one whose
351
- // path tail identifies a report with separate timeStamp and jobID segments)
352
- // redirects to, or null if the page has no legacy form or the tail lacks its shape.
350
+ // Returns the single-reportID path that a legacy two-segment report path tail (one
351
+ // identifying a report with separate timeStamp and jobID segments) canonicalizes to
352
+ // for a page under its current name, or null if the page has no legacy form or the
353
+ // tail lacks its shape.
353
354
  const getLegacyReportURL = (topic: string, pathTail: string): string | null => {
354
355
  const legacy = legacyReportTails[topic];
355
356
  if (!legacy) {
@@ -681,12 +682,21 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
681
682
  }
682
683
  // Otherwise, if it is for the home page:
683
684
  else if (['/', '/index.html'].includes(pathname)) {
684
- await recordPageMetric('pageViews', 'index');
685
- // Get the home page.
686
- const {answerPage} = await home();
687
- // Serve it.
688
- setHeaders('text/html', '/index.html', 'medium');
689
- response.end(answerPage);
685
+ // If the URL has a query string, redirect the request permanently to the
686
+ // queryless URL (see the same redirect for .html pages below).
687
+ if (search) {
688
+ response.writeHead(301, {Location: pathname});
689
+ response.end();
690
+ }
691
+ // Otherwise, i.e. if the URL has no query string:
692
+ else {
693
+ await recordPageMetric('pageViews', 'index');
694
+ // Get the home page.
695
+ const {answerPage} = await home();
696
+ // Serve it.
697
+ setHeaders('text/html', '/index.html', 'medium');
698
+ response.end(answerPage);
699
+ }
690
700
  }
691
701
  // Otherwise, if it is for the crawler specification:
692
702
  else if (pageName === 'robots.txt') {
@@ -766,18 +776,15 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
766
776
  // If this failed:
767
777
  if (isReportError(report)) {
768
778
  // This is not necessarily abuse: the link may have been generated (e.g. by
769
- // listIssues.html) before the report was pruned or rewound, or the stored report
770
- // may have become unreadable or unusable, none of which is the requester's fault.
771
- // So tell the requester only that the request was invalid, without accusing them,
772
- // but alert a manager with the real reason, since a syntactically valid report ID
773
- // should otherwise always be usable.
779
+ // listIssues.html) before the report was pruned or rewound, or the identifier
780
+ // may have been scraped from the API documentation's examples, none of which
781
+ // is the requester's fault. So tell the requester only that the request was
782
+ // invalid, without accusing them. No manager alert is sent: a missing report
783
+ // is a routine miss, and a report that exists but is unreadable or unusable
784
+ // has already been quarantined and alerted on by getReport.
774
785
  console.error(
775
786
  `Full report ${pathTail} requested but unavailable (${report.error})`
776
787
  );
777
- await sendAlert(
778
- 'Kilotest: requested full report unavailable',
779
- `Full report ${pathTail} was requested but could not be retrieved: ${report.error}`
780
- );
781
788
  await serveError({message: 'ERROR: Invalid request'}, response, true);
782
789
  }
783
790
  // Otherwise, i.e. if it succeeded:
@@ -800,20 +807,24 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
800
807
  // Otherwise, if it is for an HTML page other than the home page:
801
808
  else if (pageName.endsWith('.html')) {
802
809
  const topic = pageName.slice(0, -5);
803
- const legacyReportURL = getLegacyReportURL(topic, pathTail);
804
- // If it is for a page's old, pre-rename name:
805
- if (renamedPagePrefixes[topic]) {
806
- // Redirect the client permanently to the current page name, preserving any
807
- // further path segments and the query string.
808
- const newLocation = `/${renamedPagePrefixes[topic]}.html${pathTail ? `/${pathTail}` : ''}${search}`;
809
- response.writeHead(301, {Location: newLocation});
810
- response.end();
811
- }
812
- // Otherwise, if the URL identifies a report with the legacy two-segment form
813
- // (a timeStamp segment and a jobID segment), redirect the request permanently
814
- // to the URL with the single reportID segment, preserving the query string.
815
- else if (legacyReportURL !== null) {
816
- response.writeHead(301, {Location: `${legacyReportURL}${search}`});
810
+ // Compute the path in the current request signature: the current name if the
811
+ // page name is a pre-rename one, and the single-reportID tail if the tail
812
+ // identifies a report with the legacy two-segment form (a timeStamp segment
813
+ // and a jobID segment).
814
+ const canonicalTopic = renamedPagePrefixes[topic] ?? topic;
815
+ const canonicalPath = getLegacyReportURL(canonicalTopic, pathTail) ??
816
+ `/${canonicalTopic}.html${pathTail ? `/${pathTail}` : ''}`;
817
+ // If the request signature contains any obsolete element (a pre-rename page
818
+ // name, a legacy two-segment report tail, or a query string, which no GET
819
+ // page handler reads, since the search argument passed to answer() is
820
+ // consumed only for POST requests, where it is reconstructed from the
821
+ // request body), convert all of them at once by redirecting the request
822
+ // permanently to the canonical path with no query string. A request with a
823
+ // query otherwise returns a byte-identical duplicate of the queryless page,
824
+ // which search engines report as a duplicate without a user-selected
825
+ // canonical (e.g. the pathID queries still requested on listDiagnoses URLs).
826
+ if (canonicalPath !== pathname || search) {
827
+ response.writeHead(301, {Location: canonicalPath});
817
828
  response.end();
818
829
  }
819
830
  // Otherwise, if the page can be generated and is not POST-only (a POST-only path also matches
package/lhm.plugin.json CHANGED
@@ -96,81 +96,48 @@
96
96
  "response content": {
97
97
  "type": "object",
98
98
  "properties": {
99
+ "how to get test results about a page": {
100
+ "type": "object",
101
+ "properties": {
102
+ "method": {
103
+ "type": "string",
104
+ "const": "GET"
105
+ },
106
+ "URL": {
107
+ "type": "string",
108
+ "description": "URL template; replace {identifier} with the identifier of the latest report about the page."
109
+ }
110
+ },
111
+ "required": [
112
+ "method",
113
+ "URL"
114
+ ],
115
+ "additionalProperties": false
116
+ },
117
+ "web users can get test results about a page at": {
118
+ "type": "string",
119
+ "description": "URL template; replace {identifier} with the identifier of the latest report about the page."
120
+ },
99
121
  "basics about all tested pages": {
100
122
  "type": "array",
101
123
  "items": {
102
124
  "type": "object",
103
125
  "properties": {
104
- "tested web page": {
105
- "type": "object",
106
- "properties": {
107
- "description": {
108
- "type": "string"
109
- },
110
- "URL": {
111
- "type": "string"
112
- }
113
- },
114
- "required": [
115
- "description",
116
- "URL"
117
- ],
118
- "additionalProperties": false
119
- },
120
- "number of reports about the page": {
121
- "type": "number"
122
- },
123
- "basics about the latest report": {
124
- "type": "object",
125
- "properties": {
126
- "identifier": {
127
- "type": "string",
128
- "description": "Identifier of the report (example: 260503T0432-x9z)."
129
- },
130
- "completion date and time": {
131
- "type": "string"
132
- },
133
- "days since the report was completed": {
134
- "type": [
135
- "number",
136
- "null"
137
- ]
138
- }
139
- },
140
- "required": [
141
- "identifier",
142
- "completion date and time",
143
- "days since the report was completed"
144
- ],
145
- "additionalProperties": false
146
- },
147
- "how to get details about the latest report": {
148
- "type": "object",
149
- "properties": {
150
- "method": {
151
- "type": "string",
152
- "const": "GET"
153
- },
154
- "URL": {
155
- "type": "string"
156
- }
157
- },
158
- "required": [
159
- "method",
160
- "URL"
161
- ],
162
- "additionalProperties": false
126
+ "description": {
127
+ "type": "string"
163
128
  },
164
- "web users can get details about the latest report at": {
129
+ "URL": {
165
130
  "type": "string"
131
+ },
132
+ "identifier": {
133
+ "type": "string",
134
+ "description": "Identifier of the latest report about the page (example: 260503T0432-x9z)."
166
135
  }
167
136
  },
168
137
  "required": [
169
- "tested web page",
170
- "number of reports about the page",
171
- "basics about the latest report",
172
- "how to get details about the latest report",
173
- "web users can get details about the latest report at"
138
+ "description",
139
+ "URL",
140
+ "identifier"
174
141
  ],
175
142
  "additionalProperties": false
176
143
  }
@@ -231,6 +198,8 @@
231
198
  }
232
199
  },
233
200
  "required": [
201
+ "how to get test results about a page",
202
+ "web users can get test results about a page at",
234
203
  "basics about all tested pages",
235
204
  "how to request that a page with no report be tested",
236
205
  "how a web user can request that the page be tested"
@@ -4423,5 +4392,5 @@
4423
4392
  }
4424
4393
  }
4425
4394
  ],
4426
- "version": "60.9.1"
4395
+ "version": "61.0.0"
4427
4396
  }
package/llms-full.txt CHANGED
@@ -74,7 +74,7 @@ Kilotest offers these tools:
74
74
 
75
75
  - `listPages`
76
76
  - method: `GET`
77
- - purpose: list all tested pages, each with its latest report and its number of reports
77
+ - purpose: list all tested pages, each with the identifier of its latest report
78
78
  - path: `/api/listPages`
79
79
  - caveat: Cached results are likely to be stale and should not be used
80
80
  - `listIssues`
@@ -163,7 +163,7 @@ When you request a test or a retest:
163
163
  - approval followed by the requested testing
164
164
  - rejection
165
165
  - how to confirm approval
166
- - check test completion by calling `listPages` and looking for the page and the completion date of its latest report in the list
166
+ - check test completion by calling `listPages` and looking for the page in the list
167
167
  - you advise the user to check in approximately 1 day for the test results on the Kilotest website, starting at `https://kilotest.com/listPages.html`
168
168
 
169
169
  For immediate testing without waiting for manual approval, use `orderNewTest` or `orderRetest` instead of `requestNewTest` or `requestRetest`. Each enqueues the job right away and returns a report identifier along with a question about whether to wait for completion. To wait, call `awaitTest` with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call `listIssues` with the same identifier.
package/openapi-3.0.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). 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.'
5
- version: 60.9.1
5
+ version: 61.0.0
6
6
  license:
7
7
  name: MIT
8
8
  url: https://github.com/jrpool/kilotest/blob/main/LICENSE
@@ -60,61 +60,39 @@ paths:
60
60
  response content:
61
61
  type: object
62
62
  properties:
63
+ how to get test results about a page:
64
+ type: object
65
+ properties:
66
+ method:
67
+ type: string
68
+ enum:
69
+ - GET
70
+ URL:
71
+ type: string
72
+ description: URL template; replace {identifier} with the identifier of the latest report about the page.
73
+ required:
74
+ - method
75
+ - URL
76
+ additionalProperties: false
77
+ web users can get test results about a page at:
78
+ type: string
79
+ description: URL template; replace {identifier} with the identifier of the latest report about the page.
63
80
  basics about all tested pages:
64
81
  type: array
65
82
  items:
66
83
  type: object
67
84
  properties:
68
- tested web page:
69
- type: object
70
- properties:
71
- description:
72
- type: string
73
- URL:
74
- type: string
75
- required:
76
- - description
77
- - URL
78
- additionalProperties: false
79
- number of reports about the page:
80
- type: number
81
- basics about the latest report:
82
- type: object
83
- properties:
84
- identifier:
85
- type: string
86
- description: 'Identifier of the report (example: 260503T0432-x9z).'
87
- completion date and time:
88
- type: string
89
- days since the report was completed:
90
- type: number
91
- nullable: true
92
- required:
93
- - identifier
94
- - completion date and time
95
- - days since the report was completed
96
- additionalProperties: false
97
- how to get details about the latest report:
98
- type: object
99
- properties:
100
- method:
101
- type: string
102
- enum:
103
- - GET
104
- URL:
105
- type: string
106
- required:
107
- - method
108
- - URL
109
- additionalProperties: false
110
- web users can get details about the latest report at:
85
+ description:
86
+ type: string
87
+ URL:
111
88
  type: string
89
+ identifier:
90
+ type: string
91
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z).'
112
92
  required:
113
- - tested web page
114
- - number of reports about the page
115
- - basics about the latest report
116
- - how to get details about the latest report
117
- - web users can get details about the latest report at
93
+ - description
94
+ - URL
95
+ - identifier
118
96
  additionalProperties: false
119
97
  how to request that a page with no report be tested:
120
98
  type: object
@@ -156,6 +134,8 @@ paths:
156
134
  - URL
157
135
  additionalProperties: false
158
136
  required:
137
+ - how to get test results about a page
138
+ - web users can get test results about a page at
159
139
  - basics about all tested pages
160
140
  - how to request that a page with no report be tested
161
141
  - how a web user can request that the page be tested
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). 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.'
5
- version: 60.9.1
5
+ version: 61.0.0
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
@@ -58,61 +58,38 @@ paths:
58
58
  response content:
59
59
  type: object
60
60
  properties:
61
+ how to get test results about a page:
62
+ type: object
63
+ properties:
64
+ method:
65
+ type: string
66
+ const: GET
67
+ URL:
68
+ type: string
69
+ description: URL template; replace {identifier} with the identifier of the latest report about the page.
70
+ required:
71
+ - method
72
+ - URL
73
+ additionalProperties: false
74
+ web users can get test results about a page at:
75
+ type: string
76
+ description: URL template; replace {identifier} with the identifier of the latest report about the page.
61
77
  basics about all tested pages:
62
78
  type: array
63
79
  items:
64
80
  type: object
65
81
  properties:
66
- tested web page:
67
- type: object
68
- properties:
69
- description:
70
- type: string
71
- URL:
72
- type: string
73
- required:
74
- - description
75
- - URL
76
- additionalProperties: false
77
- number of reports about the page:
78
- type: number
79
- basics about the latest report:
80
- type: object
81
- properties:
82
- identifier:
83
- type: string
84
- description: 'Identifier of the report (example: 260503T0432-x9z).'
85
- completion date and time:
86
- type: string
87
- days since the report was completed:
88
- type:
89
- - number
90
- - 'null'
91
- required:
92
- - identifier
93
- - completion date and time
94
- - days since the report was completed
95
- additionalProperties: false
96
- how to get details about the latest report:
97
- type: object
98
- properties:
99
- method:
100
- type: string
101
- const: GET
102
- URL:
103
- type: string
104
- required:
105
- - method
106
- - URL
107
- additionalProperties: false
108
- web users can get details about the latest report at:
82
+ description:
83
+ type: string
84
+ URL:
85
+ type: string
86
+ identifier:
109
87
  type: string
88
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z).'
110
89
  required:
111
- - tested web page
112
- - number of reports about the page
113
- - basics about the latest report
114
- - how to get details about the latest report
115
- - web users can get details about the latest report at
90
+ - description
91
+ - URL
92
+ - identifier
116
93
  additionalProperties: false
117
94
  how to request that a page with no report be tested:
118
95
  type: object
@@ -153,6 +130,8 @@ paths:
153
130
  - URL
154
131
  additionalProperties: false
155
132
  required:
133
+ - how to get test results about a page
134
+ - web users can get test results about a page at
156
135
  - basics about all tested pages
157
136
  - how to request that a page with no report be tested
158
137
  - how a web user can request that the page be tested
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "60.9.1",
3
+ "version": "61.0.0",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/robots.txt CHANGED
@@ -14,4 +14,7 @@ Disallow: /rewindReportsForm.html
14
14
  Disallow: /expungeReportsForm.html
15
15
  Disallow: /renewWCAGForm.html
16
16
  Disallow: /renewWCAG.html
17
+ # JSON endpoints, whose contents are not indexable pages; crawling them is pointless.
18
+ Disallow: /api/
19
+ Disallow: /fullReport.json
17
20
  Sitemap: https://kilotest.com/sitemap.xml
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "60.9.1",
9
+ "version": "61.0.0",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",