@jrpool/kilotest 55.0.0 → 57.1.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.
Files changed (52) hide show
  1. package/README.md +1 -1
  2. package/api/awaitTest.test.ts +114 -0
  3. package/api/awaitTest.ts +108 -0
  4. package/api/listReports.ts +2 -2
  5. package/api/orderNewTest.test.ts +213 -0
  6. package/api/orderNewTest.ts +145 -0
  7. package/api/orderRetest.test.ts +179 -0
  8. package/api/orderRetest.ts +119 -0
  9. package/api/{requestTest.test.ts → requestNewTest.test.ts} +25 -25
  10. package/api/{requestTest.ts → requestNewTest.ts} +16 -39
  11. package/api/requestRetest.test.ts +6 -6
  12. package/api/requestRetest.ts +10 -33
  13. package/api/routes.test.ts +7 -4
  14. package/api/routes.ts +45 -7
  15. package/api/schemas.ts +120 -10
  16. package/api/util.ts +59 -2
  17. package/api/version.ts +6 -2
  18. package/capability.md +12 -5
  19. package/docs/SERVICE.md +6 -8
  20. package/env.example +15 -0
  21. package/index.html +1 -0
  22. package/index.test.ts +95 -24
  23. package/index.ts +83 -16
  24. package/lhm.plugin.json +1 -1
  25. package/linkValidation.test.ts +2 -2
  26. package/llms-full.txt +34 -3
  27. package/llms.txt +6 -2
  28. package/mcp.test.ts +72 -12
  29. package/mcp.ts +81 -11
  30. package/memory/MEMORY.md +1 -0
  31. package/memory/plan_issue_111_instant_test.md +198 -0
  32. package/openapi.yaml +451 -4
  33. package/package.json +1 -1
  34. package/routes.test.ts +1 -1
  35. package/routing.test.ts +2 -2
  36. package/server.json +1 -1
  37. package/sitemap.xml +2 -1
  38. package/smokeTest.ts +2 -2
  39. package/util.test.ts +214 -2
  40. package/util.ts +175 -7
  41. package/web/listReports/index.html +1 -1
  42. package/web/listReports/listReports.test.ts +1 -1
  43. package/web/privacy/index.html +37 -0
  44. package/web/privacy/index.ts +20 -0
  45. package/web/privacy/privacy.test.ts +18 -0
  46. package/web/{requestTest → requestNewTest}/index.ts +1 -1
  47. package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +2 -2
  48. package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
  49. package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
  50. package/web/tutorialAI/index.html +26 -39
  51. /package/web/{requestTest → requestNewTest}/index.html +0 -0
  52. /package/web/{requestTestForm → requestNewTestForm}/index.ts +0 -0
@@ -6,7 +6,7 @@
6
6
  // IMPORTS
7
7
 
8
8
  import {z} from 'zod';
9
- import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
9
+ import {buildRequestDisposition, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
10
10
  import {checkLength, isJobID, isTimeStamp, processTestRequest} from '../util.ts';
11
11
  import {requestRetestResponseSchema} from './schemas.ts';
12
12
 
@@ -60,38 +60,15 @@ export const response = async (args: string[]) => {
60
60
  'reason why the page should be retested': reason
61
61
  };
62
62
  // Add information about the disposition of the request.
63
- if (requestResult === 'ok') {
64
- requestDisposition = {
65
- 'what happens next': 'Your request is likely to be approved and processed within 1 hour to 1 day.',
66
- 'how you can check for completion': 'You can call the listReports tool to learn whether the page has been retested and a new report is available.',
67
- 'how a web user can check for completion': `A web user can visit ${thisHost}/listReports.html to learn whether the page has been retested and a new report is available.`
68
- };
69
- }
70
- else {
71
- const failureFact = 'Your request will not be processed, because ';
72
- let failureReason: string;
73
- if (requestResult === 'description') {
74
- failureReason = 'a request to test a page with the same description is already approved.'
75
- }
76
- else if (requestResult === 'url') {
77
- failureReason = 'a request to test a page with the same URL is already approved.'
78
- }
79
- else if (requestResult === 'superseded') {
80
- failureReason = 'a later report about a page with the same description is available.'
81
- }
82
- else if (requestResult === 'queueFull') {
83
- failureReason = 'too many requests are awaiting approval right now. Please try again later, ' +
84
- 'or post your request at https://github.com/jrpool/kilotest/issues or email info@kilotest.com.'
85
- }
86
- else {
87
- failureReason = 'an identical request is already awaiting approval.'
88
- }
89
- requestDisposition = {
90
- 'what happens next': `${failureFact}${failureReason}`,
91
- 'how you can check for completion': 'Not applicable.',
92
- 'how a web user can check for completion': 'Not applicable.'
93
- }
94
- }
63
+ requestDisposition = buildRequestDisposition(
64
+ requestResult,
65
+ {
66
+ whatHappensNext: 'Your request is likely to be approved and processed within 1 hour to 1 day.',
67
+ howToCheck: 'You can call the listReports tool to learn whether the page has been retested and a new report is available.',
68
+ howWebUserChecks: `A web user can visit ${thisHost}/listReports.html to learn whether the page has been retested and a new report is available.`
69
+ },
70
+ {result: 'superseded', reason: 'a later report about a page with the same description is available.'}
71
+ );
95
72
  }
96
73
  }
97
74
  // Create the response content.
@@ -11,22 +11,25 @@ import {routes} from './routes.ts';
11
11
 
12
12
  // TESTS
13
13
 
14
- test('routes exports an array with 8 operations', () => {
14
+ test('routes exports an array with 11 operations', () => {
15
15
  assert.ok(Array.isArray(routes));
16
- assert.equal(routes.length, 8);
16
+ assert.equal(routes.length, 11);
17
17
  });
18
18
 
19
19
  test('routes includes all expected operation IDs', () => {
20
20
  const ids = routes.map(r => r.operationId).sort();
21
21
  assert.deepEqual(ids, [
22
+ 'awaitTest',
22
23
  'getReport',
23
24
  'listDiagnoses',
24
25
  'listIssues',
25
26
  'listReports',
26
27
  'listViolators',
28
+ 'orderNewTest',
29
+ 'orderRetest',
27
30
  'requestFeature',
28
- 'requestRetest',
29
- 'requestTest'
31
+ 'requestNewTest',
32
+ 'requestRetest'
30
33
  ]);
31
34
  });
32
35
 
package/api/routes.ts CHANGED
@@ -11,17 +11,23 @@ import {
11
11
  listViolatorsSchema,
12
12
  listDiagnosesSchema,
13
13
  getReportSchema,
14
- requestTestSchema,
14
+ requestNewTestSchema,
15
15
  requestRetestSchema,
16
16
  requestFeatureSchema,
17
+ orderNewTestSchema,
18
+ orderRetestSchema,
19
+ awaitTestSchema,
17
20
  listReportsResponseSchema,
18
21
  listIssuesResponseSchema,
19
22
  listViolatorsResponseSchema,
20
23
  listDiagnosesResponseSchema,
21
24
  getReportResponseSchema,
22
- requestTestResponseSchema,
25
+ requestNewTestResponseSchema,
23
26
  requestRetestResponseSchema,
24
- requestFeatureResponseSchema
27
+ requestFeatureResponseSchema,
28
+ orderNewTestResponseSchema,
29
+ orderRetestResponseSchema,
30
+ awaitTestResponseSchema
25
31
  } from './schemas.ts';
26
32
 
27
33
  // ROUTES
@@ -67,12 +73,12 @@ export const routes = [
67
73
  responseSchema: getReportResponseSchema
68
74
  },
69
75
  {
70
- operationId: 'requestTest',
76
+ operationId: 'requestNewTest',
71
77
  method: 'post',
72
- path: '/api/requestTest',
78
+ path: '/api/requestNewTest',
73
79
  summary: 'Request that a page be tested',
74
- bodySchema: z.object(requestTestSchema),
75
- responseSchema: requestTestResponseSchema
80
+ bodySchema: z.object(requestNewTestSchema),
81
+ responseSchema: requestNewTestResponseSchema
76
82
  },
77
83
  {
78
84
  operationId: 'requestRetest',
@@ -93,5 +99,37 @@ export const routes = [
93
99
  summary: 'Request a feature improvement or new feature',
94
100
  bodySchema: z.object(requestFeatureSchema),
95
101
  responseSchema: requestFeatureResponseSchema
102
+ },
103
+ {
104
+ operationId: 'orderNewTest',
105
+ method: 'post',
106
+ path: '/api/orderNewTest',
107
+ summary: 'Order that a page be tested immediately, without manual approval',
108
+ bodySchema: z.object(orderNewTestSchema),
109
+ responseSchema: orderNewTestResponseSchema
110
+ },
111
+ {
112
+ operationId: 'orderRetest',
113
+ method: 'post',
114
+ path: '/api/orderRetest/{timeStamp}/{jobID}',
115
+ summary: 'Order that a page be retested immediately, without manual approval',
116
+ pathParamsSchema: z.object({
117
+ timeStamp: orderRetestSchema.timeStamp,
118
+ jobID: orderRetestSchema.jobID
119
+ }),
120
+ bodySchema: z.object({reason: orderRetestSchema.reason}),
121
+ responseSchema: orderRetestResponseSchema
122
+ },
123
+ {
124
+ operationId: 'awaitTest',
125
+ method: 'post',
126
+ path: '/api/awaitTest/{timeStamp}/{jobID}',
127
+ summary: 'Wait for a job ordered via orderNewTest or orderRetest to complete',
128
+ pathParamsSchema: z.object(awaitTestSchema),
129
+ // No caller-supplied fields beyond the path parameters; an empty body schema (rather
130
+ // than omitting bodySchema) keeps this route consistent with the "every POST route
131
+ // has a bodySchema" invariant the rest of this table and its own test maintain.
132
+ bodySchema: z.object({}),
133
+ responseSchema: awaitTestResponseSchema
96
134
  }
97
135
  ];
package/api/schemas.ts CHANGED
@@ -41,8 +41,8 @@ export const getReportSchema = {
41
41
  jobID: z.string().describe('Job identifier of the report (example: x9z)')
42
42
  };
43
43
 
44
- // requestTest: POST /api/requestTest
45
- export const requestTestSchema = {
44
+ // requestNewTest: POST /api/requestNewTest
45
+ export const requestNewTestSchema = {
46
46
  description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'),
47
47
  URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
48
48
  reason: z.string().describe('20- to 100-character reason why the page should be tested')
@@ -60,6 +60,26 @@ export const requestFeatureSchema = {
60
60
  feature: z.string().describe('20- to 1000-character description of requested feature improvement or new feature')
61
61
  };
62
62
 
63
+ // orderNewTest: POST /api/orderNewTest
64
+ export const orderNewTestSchema = {
65
+ description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'),
66
+ URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
67
+ reason: z.string().describe('20- to 100-character reason why the page should be tested')
68
+ };
69
+
70
+ // orderRetest: POST /api/orderRetest/{timeStamp}/{jobID}
71
+ export const orderRetestSchema = {
72
+ timeStamp: z.string().describe('Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'),
73
+ jobID: z.string().describe('Job identifier of the latest report about the page (example: x9z)'),
74
+ reason: z.string().describe('20- to 100-character reason why the page should be retested')
75
+ };
76
+
77
+ // awaitTest: POST /api/awaitTest
78
+ export const awaitTestSchema = {
79
+ timeStamp: z.string().describe('Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)'),
80
+ jobID: z.string().describe('Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)')
81
+ };
82
+
63
83
  // listReports takes no input; omitted (mcp.ts already uses inputSchema: {}).
64
84
 
65
85
  // RESPONSE SCHEMAS
@@ -93,32 +113,40 @@ const toolsFactsSchema = z.object({
93
113
  'web users can obtain similar functionalities at': z.string()
94
114
  }).meta({id: 'ToolsFacts'});
95
115
 
96
- // Identifies a related request (e.g., the closest ancestor request in the drill-down hierarchy).
116
+ // Identifies a related request (e.g., the closest ancestor request in the drill-down
117
+ // hierarchy). method is usually GET (every drill-down tool's ancestor is a read), except
118
+ // for orderNewTest/orderRetest, whose own ancestor is a GET, and awaitTest, whose ancestors
119
+ // (orderNewTest and orderRetest) are both POST (they create a job).
97
120
  const requestReferenceSchema = z.object({
98
121
  'tool name': z.string(),
99
122
  description: z.string(),
100
- method: z.literal('GET'),
123
+ method: z.enum(['GET', 'POST']),
101
124
  URL: z.string()
102
125
  }).meta({id: 'RequestReference'});
103
126
 
104
127
  // GET endpoints have no 'body'; POST endpoints get a request-specific body schema.
105
- const thisRequestSchema = (method: string, bodySchema?: ZodTypeAny) => z.object({
128
+ // ancestorSchema overrides the default single-ancestor shape for a tool whose closest
129
+ // ancestor is not unique: awaitTest can be reached from either orderNewTest or orderRetest,
130
+ // and the identifier alone does not say which, so it reports both as an array rather
131
+ // than arbitrarily picking one.
132
+ const thisRequestSchema = (method: string, bodySchema?: ZodTypeAny, ancestorSchema: ZodTypeAny = requestReferenceSchema.nullable()) => z.object({
106
133
  description: z.string(),
107
134
  method: z.literal(method),
108
135
  URL: z.string(),
109
136
  ...(bodySchema ? {body: bodySchema} : {}),
110
- 'closest ancestor request': requestReferenceSchema.nullable()
137
+ 'closest ancestor request': ancestorSchema
111
138
  });
112
139
 
113
140
  const envelope = <T extends ZodTypeAny>(
114
141
  method: string,
115
142
  responseContentSchema: T,
116
143
  bodySchema?: ZodTypeAny,
117
- similarWebSchema: ZodTypeAny = similarWebRequestsSchema
144
+ similarWebSchema: ZodTypeAny = similarWebRequestsSchema,
145
+ ancestorSchema?: ZodTypeAny
118
146
  ) => z.object({
119
147
  'tool collection': toolsFactsSchema,
120
148
  'tool name': z.string(),
121
- 'this request': thisRequestSchema(method, bodySchema),
149
+ 'this request': thisRequestSchema(method, bodySchema, ancestorSchema),
122
150
  'URLs of similar requests for web users': similarWebSchema,
123
151
  'response metadata': responseMetadataSchema,
124
152
  'response content': responseContentSchema
@@ -268,7 +296,7 @@ export const getReportResponseSchema = envelope('GET', z.object({
268
296
  'full report': z.unknown().describe('The full raw Testaro/Testilo report JSON, copied verbatim, or an error object if it could not be retrieved.')
269
297
  }));
270
298
 
271
- export const requestTestResponseSchema = envelope(
299
+ export const requestNewTestResponseSchema = envelope(
272
300
  'POST',
273
301
  z.object({
274
302
  'details about your request': z.union([
@@ -285,7 +313,7 @@ export const requestTestResponseSchema = envelope(
285
313
  'how a web user can check for completion': z.string()
286
314
  }).nullable()
287
315
  }),
288
- z.object(requestTestSchema)
316
+ z.object(requestNewTestSchema)
289
317
  );
290
318
 
291
319
  export const requestRetestResponseSchema = envelope(
@@ -320,3 +348,85 @@ export const requestFeatureResponseSchema = envelope(
320
348
  z.object(requestFeatureSchema),
321
349
  z.null()
322
350
  );
351
+
352
+ // Identifies the report a successfully ordered or awaited job will produce, once complete.
353
+ const reportIdentifierSchema = z.object({
354
+ timeStamp: z.string().describe('Timestamp to pass to listIssues once the report is ready.'),
355
+ jobID: z.string().describe('Job identifier to pass to listIssues once the report is ready.')
356
+ }).meta({id: 'ReportIdentifier'});
357
+
358
+ export const orderNewTestResponseSchema = envelope(
359
+ 'POST',
360
+ z.object({
361
+ 'details about your order': z.union([
362
+ z.object({error: z.string()}),
363
+ z.object({
364
+ 'date and time received': z.string(),
365
+ 'page to be tested': z.object({description: z.string(), URL: z.string()}),
366
+ 'reason why the page should be tested': z.string()
367
+ })
368
+ ]),
369
+ 'disposition of your order': z.object({
370
+ 'what happens next': z.string(),
371
+ 'report identifier': reportIdentifierSchema.nullable(),
372
+ 'how you can check for completion': z.string(),
373
+ 'do you want to wait for completion': z.string()
374
+ }).nullable()
375
+ }),
376
+ z.object(orderNewTestSchema),
377
+ z.object({
378
+ 'this request': z.null(),
379
+ 'closest ancestor request': z.string().nullable()
380
+ })
381
+ );
382
+
383
+ export const orderRetestResponseSchema = envelope(
384
+ 'POST',
385
+ z.object({
386
+ 'details about your order': z.union([
387
+ z.object({error: z.string()}),
388
+ z.object({
389
+ 'date and time received': z.string(),
390
+ 'page to be retested': z.object({description: z.string(), URL: z.string()}),
391
+ 'reason why the page should be retested': z.string()
392
+ })
393
+ ]),
394
+ 'disposition of your order': z.object({
395
+ 'what happens next': z.string(),
396
+ 'report identifier': reportIdentifierSchema.nullable(),
397
+ 'how you can check for completion': z.string(),
398
+ 'do you want to wait for completion': z.string()
399
+ }).nullable()
400
+ }),
401
+ z.object({reason: orderRetestSchema.reason}),
402
+ z.object({
403
+ 'this request': z.null(),
404
+ 'closest ancestor request': z.string().nullable()
405
+ })
406
+ );
407
+
408
+ export const awaitTestResponseSchema = envelope(
409
+ 'POST',
410
+ z.object({
411
+ 'details about your wait': z.union([
412
+ z.object({error: z.string()}),
413
+ z.object({
414
+ 'report identifier': reportIdentifierSchema
415
+ })
416
+ ]),
417
+ 'disposition of your wait': z.object({
418
+ outcome: z.enum(['completed', 'failed', 'timedOut']),
419
+ 'what happened': z.string(),
420
+ 'how you can check for completion': z.string()
421
+ }).nullable()
422
+ }),
423
+ undefined,
424
+ z.object({
425
+ 'this request': z.null(),
426
+ 'closest ancestor request': z.null()
427
+ }),
428
+ // awaitTest can be reached from either orderNewTest or orderRetest, and its own
429
+ // parameters (timeStamp/jobID) do not say which one produced them, so both possible
430
+ // ancestors are reported, rather than arbitrarily naming just one as "the" ancestor.
431
+ z.array(requestReferenceSchema).length(2)
432
+ );
package/api/util.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  objectSort,
16
16
  ruleEngines
17
17
  } from '../util.ts';
18
- import type {ReportExtract} from '../util.ts';
18
+ import type {ReportExtract, TestRequestResult} from '../util.ts';
19
19
  import {issues as issueSpecs} from 'testaro-issues';
20
20
 
21
21
  // TYPES
@@ -29,6 +29,15 @@ export type ReportBasics = {
29
29
  'whether a later report about the same page exists': boolean;
30
30
  };
31
31
 
32
+ // The disposition of a test/retest/instant-test request, as returned by
33
+ // buildRequestDisposition, or null when the request itself was invalid (in which case
34
+ // details about your request already carries the error, and no disposition applies).
35
+ export type RequestDisposition = {
36
+ 'what happens next': string;
37
+ 'how you can check for completion': string;
38
+ 'how a web user can check for completion': string;
39
+ } | null;
40
+
32
41
  // FUNCTIONS
33
42
 
34
43
  // Returns the base URL of this Kilotest host.
@@ -50,7 +59,7 @@ export const getToolsFacts = () => ({
50
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.',
51
60
  },
52
61
  'how to generate more findings': {
53
- 'new testing': 'If no report is available yet about a page, use the requestTest tool to request that it be tested.',
62
+ 'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.',
54
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.',
55
64
  'latency': 'Requests for testing and retesting are usually approved and fulfilled within one day.',
56
65
  'confirmation': 'Use the listReports tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
@@ -59,6 +68,54 @@ export const getToolsFacts = () => ({
59
68
  'URL': `${getThisHost()}/mcp`,
60
69
  'web users can obtain similar functionalities at': getThisHost()
61
70
  });
71
+ // Returns the human-readable reason a test/retest/instant-test request was not
72
+ // processed, given the result processTestRequest returned. requestResult is always a
73
+ // rejection reason here ('description', 'url', 'queueFull', or an unrecognized value
74
+ // meaning 'duplicate'); callers pass their own extra branch for a requestResult value
75
+ // specific to their own request type ('reportExists' for requestNewTest, 'superseded' for
76
+ // requestRetest), since that is the one branch not shared between them.
77
+ export const getRequestFailureReason = (
78
+ requestResult: Exclude<TestRequestResult, 'ok'>,
79
+ extraBranch?: {result: TestRequestResult; reason: string}
80
+ ): string => {
81
+ if (requestResult === 'description') {
82
+ return 'a request to test a page with the same description is already approved.';
83
+ }
84
+ if (requestResult === 'url') {
85
+ return 'a request to test a page with the same URL is already approved.';
86
+ }
87
+ if (extraBranch && requestResult === extraBranch.result) {
88
+ return extraBranch.reason;
89
+ }
90
+ if (requestResult === 'queueFull') {
91
+ return 'too many requests are awaiting approval right now. Please try again later, ' +
92
+ 'or post your request at https://github.com/jrpool/kilotest/issues or email info@kilotest.com.';
93
+ }
94
+ return 'an identical request is already awaiting approval.';
95
+ };
96
+ // Returns the disposition of a test/retest/instant-test request: an 'ok' disposition
97
+ // describing how to check for completion, or a rejection disposition naming why the
98
+ // request was not processed. Shared by requestNewTest, requestRetest, and orderNewTest,
99
+ // whose only differences are the wording of the 3 completion-related sentences and, for
100
+ // a rejection, the one extra requestResult branch getRequestFailureReason takes.
101
+ export const buildRequestDisposition = (
102
+ requestResult: TestRequestResult,
103
+ okText: {whatHappensNext: string; howToCheck: string; howWebUserChecks: string},
104
+ extraBranch?: {result: TestRequestResult; reason: string}
105
+ ): RequestDisposition => {
106
+ if (requestResult === 'ok') {
107
+ return {
108
+ 'what happens next': okText.whatHappensNext,
109
+ 'how you can check for completion': okText.howToCheck,
110
+ 'how a web user can check for completion': okText.howWebUserChecks
111
+ };
112
+ }
113
+ return {
114
+ 'what happens next': `Your request will not be processed, because ${getRequestFailureReason(requestResult, extraBranch)}`,
115
+ 'how you can check for completion': 'Not applicable.',
116
+ 'how a web user can check for completion': 'Not applicable.'
117
+ };
118
+ };
62
119
  // Returns the facts about a rule engine.
63
120
  export const getRuleEngineFacts = (ruleEngineID: string) => {
64
121
  const ruleEngineData = ruleEngines[ruleEngineID] || [null, null];
package/api/version.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  /*
2
2
  version.ts
3
- Single source of truth for the version of the Kilotest API and MCP tool contract, consumed by mcp.ts and scripts/generate-openapi.ts.
3
+ Single source of truth for the version of the Kilotest API and MCP tool contract, consumed by mcp.ts and scripts/generate-openapi.ts. Re-exports package.json's version field verbatim, rather than hand-maintaining a separate literal here, so MCP and OpenAPI consumers see package.json's version exactly, including minor/patch bumps from changes that do not affect the API contract; this is a deliberate choice, not an oversight.
4
4
  */
5
5
 
6
+ // IMPORTS
7
+
8
+ import pkg from '../package.json' with {type: 'json'};
9
+
6
10
  // CONSTANTS
7
11
 
8
- export const version = '2.1.0';
12
+ export const version = pkg.version;
package/capability.md CHANGED
@@ -6,7 +6,7 @@ Web-page front-end quality audit. Kilotest tests web pages for front-end quality
6
6
 
7
7
  ## Purpose
8
8
 
9
- Language models and AI platforms often need accurate, page-specific data about the front-end quality of a web page to answer user questions. Rule engines, such as `axe-core`, `QualWeb`, and `WAVE`, provide such data, but with large coverage gaps. Orchestrating and reconciling numerous complementary rule engines would be difficult. Kilotest solves that problem by testing pages with an ensemble of numerous independent rule engines, making their results comparable, saving the results as JSON reports, mining the reports for facts at selectable levels of detail.
9
+ Language models and AI platforms often need accurate, page-specific data about the front-end quality of a web page to answer user questions. Rule engines, such as `axe-core`, `QualWeb`, and `WAVE`, provide such data, but with large coverage gaps. Orchestrating and reconciling numerous complementary rule engines would be difficult. Kilotest solves that problem by testing pages with an ensemble of numerous independent rule engines, making their results comparable, saving the results as JSON reports, and mining the reports for facts at selectable levels of detail.
10
10
 
11
11
  ## Interfaces
12
12
 
@@ -26,17 +26,20 @@ A list of available reports.
26
26
 
27
27
  ## Subsequent inputs
28
28
 
29
- - If there is no report about the wanted page, a request to test it, with a page description and its URL.
30
- - If there is a report about the wanted page, requests for findings from the report at successive levels of detail.
29
+ - If there is no report about the wanted page, a request or order for a new test, with a page description, its URL, and a reason for testing it.
30
+ - If there is already a report about the wanted page, requests for findings from the report at successive levels of detail, or, if the report is deemed obsolete, a request or order for a retest, with a reason for retesting it.
31
+ - Complaints and suggestions about features of Kilotest.
31
32
 
32
33
  ## Subsequent outputs
33
34
 
34
- - From a request to test a page: confirmation of receipt. Upon approval and execution, the list of available reports will include the new report.
35
+ - From a **request** for a new test or a retest: if valid, confirmation of receipt. If the request is manager-approved and executed, the list of available reports will include the new report, typically in 1 hour to 1 day.
36
+ - From an **order** for a new test or a retest: if valid, confirmation of receipt and immediate approval or rejection. If the request is approved, the output will include the identifier of the forthcoming report and two methods for getting the report: (1) allowing time (2 to 4 minutes) for completion of the report and then requesting it, and (2) making a request whose response will await the report and then link to it. Orders are enabled in the MCP and API interfaces only.
35
37
  - From a request for findings: Details about issues reported, related WCAG standards, what rules were violated, and which DOM elements violated them.
38
+ - From complaints and suggestions about features: acknowledgment of receipt and of the manager being notified.
36
39
 
37
40
  ## Side effects
38
41
 
39
- Kilotest reads the specified web page during testing. It does not modify the target site. Testing is asynchronous and may take up to approximately one day before reports are available for newly requested pages.
42
+ Kilotest reads the specified web page during testing. It does not modify the target site.
40
43
 
41
44
  ## Cost
42
45
 
@@ -49,3 +52,7 @@ MIT.
49
52
  ## Repository
50
53
 
51
54
  `https://github.com/jrpool/kilotest`
55
+
56
+ ## Privacy
57
+
58
+ Kilotest requires no account. See the [privacy policy](https://kilotest.com/privacy.html).
package/docs/SERVICE.md CHANGED
@@ -270,7 +270,7 @@ kilotest.com {
270
270
  }
271
271
  @allowedPOST {
272
272
  method POST
273
- path /mcp /requestRetest.html/* /requestTest.html /requestAction.html /reannotate.html /renewWCAG.html /ai0BalanceForm.html /deleteNotesForm.html /expungeReportsForm.html /hideReportForm.html /metrics.html /pruneReportsForm.html /rewindReportsForm.html /showHiddenReportsForm.html /unhideReportForm.html /api/* /tutorialWebComment.html /tutorialAIComment.html /worker/job /worker/report
273
+ path /mcp /requestRetest.html/* /requestNewTest.html /requestAction.html /reannotate.html /renewWCAG.html /ai0BalanceForm.html /deleteNotesForm.html /expungeReportsForm.html /hideReportForm.html /metrics.html /pruneReportsForm.html /rewindReportsForm.html /showHiddenReportsForm.html /unhideReportForm.html /api/* /tutorialWebComment.html /tutorialAIComment.html /worker/job /worker/report
274
274
  }
275
275
  # Respond to OPTIONS requests.
276
276
  @allowedOPTIONS method OPTIONS
@@ -395,15 +395,13 @@ Alerts are sent to the maintainer in the following conditions:
395
395
 
396
396
  5. **Requested full report unavailable**: When a user requests a full report via the API but the report cannot be retrieved (e.g., the report file is missing from disk), an alert is sent with subject `"Kilotest: requested full report unavailable"`, including details on which report was requested and why it could not be retrieved.
397
397
 
398
- 6. **Test request received via UI**: When a user submits a test request through the web interface, an alert is sent with subject `"Kilotest: new test request in the UI"` (via `/requestTest.html`) or `"Kilotest: new retest request in the UI"` (via `/requestRetest.html`), including the target URL, description, and the user's stated reason for the request. `test` and `retest` here are the only two literal values Kilotest passes as the request's type; they are hardcoded at each call site, not user-supplied.
398
+ 6. **Request for a test or retest received**: When a user submits a request to test or retest a page, whether through the web interface (`/requestNewTest.html` or `/requestRetest.html`) or the API (the `requestNewTest` or `requestRetest` operation), an alert is sent with subject `"Kilotest: request for a new test awaits approval"` or `"Kilotest: request for a retest awaits approval"`, including the target URL, description, and the user's stated reason for the request. Both the UI and API paths funnel through the same underlying request-queuing function, so the subject and the rest of this behavior are identical regardless of which path the request arrived through. A request is treated as a duplicate, and no alert is sent for it, only if a request already pending in `db/jobs/testRequests.json`, i.e., submitted but not yet manually approved by the maintainer into an actual job, for the same URL has the identical description; this check ignores the stated reason and the test-versus-retest distinction, and is not limited to a recent time window, it compares against every request still pending approval for that URL. Once the maintainer approves a pending request (turning it into a job via `/enqueue.html`), all pending requests for that URL are cleared, so this check cannot detect a duplicate against a request that has already been approved, is currently running, or has already completed; an identical request submitted after approval alerts again.
399
399
 
400
- 7. **Test request received via API**: When a test request is submitted via the API, an alert is sent with subject `"Kilotest: new test request in the API"` (via the `requestTest` API operation) or `"Kilotest: new retest request in the API"` (via the `requestRetest` API operation), including the target URL, description, and the user's stated reason. A request is treated as a duplicate, and no alert is sent for it, only if a request already pending in `db/jobs/testRequests.json`, i.e., submitted but not yet manually approved by the maintainer into an actual job, for the same URL has the identical description; this check ignores the stated reason and the test-versus-retest distinction, and is not limited to a recent time window, it compares against every request still pending approval for that URL. Once the maintainer approves a pending request (turning it into a job via `/enqueue.html`), all pending requests for that URL are cleared, so this check cannot detect a duplicate against a request that has already been approved, is currently running, or has already completed; an identical request submitted after approval alerts again. The check and its dedup key are identical for the UI and API paths, since both funnel through the same underlying request-queuing function.
400
+ 7. **MCP feature request received**: When a user submits a feature request via the MCP server's `requestFeature` tool, an alert is sent with subject `"MCP feature request received"`, including the text of the feature request, and the request is also appended to `db/featureRequests.json`, the same way tutorial comments are appended to their own files (see item 1), so that a manager can review or delete it later via `/deleteNotesForm.html`.
401
401
 
402
- 8. **MCP feature request received**: When a user submits a feature request via the MCP server's `requestFeature` tool, an alert is sent with subject `"MCP feature request received"`, including the text of the feature request, and the request is also appended to `db/featureRequests.json`, the same way tutorial comments are appended to their own files (see item 1), so that a manager can review or delete it later via `/deleteNotesForm.html`.
402
+ 8. **WAVE balance low**: When Kilotest detects that the account balance for the WAVE API service (used for accessibility testing) has fallen below a threshold, an alert is sent with subject `"Kilotest: WAVE balance low"`, indicating the number of credits remaining.
403
403
 
404
- 9. **WAVE balance low**: When Kilotest detects that the account balance for the WAVE API service (used for accessibility testing) has fallen below a threshold, an alert is sent with subject `"Kilotest: WAVE balance low"`, indicating the number of credits remaining.
405
-
406
- 10. **AI service balance low**: When Kilotest detects that the account balance for an AI service account (such as Anthropic) has fallen below a threshold, an alert is sent with subject `"Kilotest: AI service [N] balance low"`, indicating the approximate remaining balance in dollars and the estimated cost per job.
404
+ 9. **AI service balance low**: When Kilotest detects that the account balance for an AI service account (such as Anthropic) has fallen below a threshold, an alert is sent with subject `"Kilotest: AI service [N] balance low"`, indicating the approximate remaining balance in dollars and the estimated cost per job.
407
405
 
408
406
  ### Alert configuration
409
407
 
@@ -502,7 +500,7 @@ Kilotest records basic counts of how it is used, so the maintainer can answer qu
502
500
 
503
501
  - The generic `.html` GET dispatch in `index.ts`, once per successfully served web page, recorded under the `pageViews` category by page name (for example `tutorialWeb`, `listReports`), unless the page is a manager-only page (see "Manager page activity" below), in which case it is recorded there instead.
504
502
  - The home page (`/` and `/index.html`), served by its own branch in `index.ts` rather than through the generic `.html` dispatch, recorded under `pageViews` as `index`.
505
- - Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listReports`, `requestTest`).
503
+ - Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listReports`, `requestNewTest`).
506
504
  - The `/api/*` GET and POST service branches in `index.ts`, once per call, recorded under the `apiOperations` category by operation name.
507
505
 
508
506
  `db/metrics.json` also stores a `since` time stamp, set when the file is first created, so the counts can be read as "since this date" rather than assumed to cover Kilotest's entire history. `getMetrics()` backfills any category absent from an existing `db/metrics.json` (for example one written before a category such as `managerActivity` existed), so an older file remains readable rather than causing every subsequent request to error.
package/env.example CHANGED
@@ -47,6 +47,21 @@ TUTORIAL_WEB_COMMENTS_MAX=__placeholder__
47
47
  TUTORIAL_AI_COMMENTS_MAX=__placeholder__
48
48
  # Maximum number of feature requests held via the MCP server's requestFeature tool.
49
49
  FEATURE_REQUESTS_MAX=__placeholder__
50
+ # Maximum number of jobs allowed in the job queue (db/jobs/queue) at once. Shared by every
51
+ # path that enqueues a job: manager approval of a pending test/retest request, and the
52
+ # orderNewTest/orderRetest MCP tools, which enqueue directly.
53
+ JOB_QUEUE_MAX=__placeholder__
54
+
55
+ # Timing for the awaitTest MCP tool's wait on a job ordered via orderNewTest/orderRetest.
56
+ # Defaults: 2000ms poll interval, 300000ms (5 minutes) maximum wait. The maximum wait
57
+ # deliberately exceeds the "occasionally up to 4 minutes" callers are told to expect, to
58
+ # leave margin so a job that is genuinely still running is not mistaken for one that
59
+ # never will finish. Lower values are useful for local testing.
60
+
61
+ # Milliseconds between polls while awaitTest waits for a job to complete.
62
+ AWAIT_JOB_POLL_MS=__placeholder__
63
+ # Maximum milliseconds awaitTest waits before giving up.
64
+ AWAIT_JOB_TIMEOUT_MS=__placeholder__
50
65
 
51
66
  # Alert configuration:
52
67
 
package/index.html CHANGED
@@ -90,6 +90,7 @@
90
90
  <li><a href="listReports.html">Which web pages have been tested?</a></li>
91
91
  <li><a href="listTopIssues.html">Which issues have been reported?</a></li>
92
92
  <li><a href="manage.html">What can Kilotest managers do?</a></li>
93
+ <li><a href="privacy.html">What is the privacy policy?</a></li>
93
94
  </ul>
94
95
  </main>
95
96
  </body>