@jrpool/kilotest 46.1.0 → 47.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +8 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +75 -46
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +462 -137
  48. package/{index.js → index.ts} +388 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -87
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -126
  133. package/util.js +0 -733
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,14 +1,14 @@
1
1
  /*
2
- util.js
2
+ util.ts
3
3
  Utilities for API requests.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {sendAlert} = require('../alerts');
9
- const {
8
+ import {sendAlert} from '../alerts.ts';
9
+ import {
10
10
  getAgoDays,
11
- getLatestReportExtracts,
11
+ getReportExtracts,
12
12
  getNowStamp,
13
13
  getPlainText,
14
14
  getRandomString,
@@ -16,23 +16,33 @@ const {
16
16
  getReportStats,
17
17
  objectSort,
18
18
  ruleEngines,
19
- updateRecs
20
- } = require('../util');
21
- const {issues: issueSpecs} = require('testaro-issues');
19
+ addTestRequest
20
+ } from '../util.ts';
21
+ import type {ReportExtract} from '../util.ts';
22
+ import {issues as issueSpecs} from 'testaro-issues';
22
23
 
23
- // CONSTANTS
24
+ // TYPES
24
25
 
25
- const thisHost = exports.thisHost = process.env.THIS_KILOTEST_HOST;
26
+ // The basics about a report, as returned by getReportBasics.
27
+ export type ReportBasics = {
28
+ identifier: string;
29
+ 'completion date and time': string;
30
+ 'days since the report was completed': number | null;
31
+ 'tested web page': {description: string; URL: string};
32
+ 'whether a later report about the same page exists': boolean;
33
+ };
26
34
 
27
35
  // FUNCTIONS
28
36
 
37
+ // Returns the base URL of this Kilotest host.
38
+ export const getThisHost = () => process.env.THIS_KILOTEST_HOST;
29
39
  // Returns uniform metadata for every response.
30
- exports.getResponseMetadata = () => ({
40
+ export const getResponseMetadata = () => ({
31
41
  identifier: `${getNowStamp()}-${getRandomString(3)}`,
32
42
  'date and time': new Date().toISOString()
33
43
  });
34
44
  // Returns facts about the tool collection (Kilotest).
35
- exports.getToolsFacts = () => ({
45
+ export const getToolsFacts = () => ({
36
46
  'name': 'Kilotest',
37
47
  'description': {
38
48
  'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) 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 issues and the violations retrievable at four levels of granularity.',
@@ -49,11 +59,11 @@ exports.getToolsFacts = () => ({
49
59
  '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.'
50
60
  }
51
61
  },
52
- 'URL': `${thisHost}/mcp`,
53
- 'web users can obtain similar functionalities at': thisHost
62
+ 'URL': `${getThisHost()}/mcp`,
63
+ 'web users can obtain similar functionalities at': getThisHost()
54
64
  });
55
65
  // Returns the facts about a rule engine.
56
- const getRuleEngineFacts = exports.getRuleEngineFacts = ruleEngineID => {
66
+ export const getRuleEngineFacts = (ruleEngineID: string) => {
57
67
  const ruleEngineData = ruleEngines[ruleEngineID] || [null, null];
58
68
  return {
59
69
  identifier: ruleEngineID,
@@ -62,50 +72,69 @@ const getRuleEngineFacts = exports.getRuleEngineFacts = ruleEngineID => {
62
72
  };
63
73
  };
64
74
  // Returns the facts about rule engines.
65
- exports.getRuleEnginesFacts = ruleEngineIDSet => {
75
+ export const getRuleEnginesFacts = (ruleEngineIDSet: Iterable<string>) => {
66
76
  const ruleEnginesFacts = Array.from(ruleEngineIDSet).map(id => getRuleEngineFacts(id));
67
77
  objectSort(ruleEnginesFacts, 'name', 'alpha');
68
78
  return ruleEnginesFacts;
69
79
  };
70
80
  // Returns the basics about a report.
71
- exports.getReportBasics = async (timeStamp, jobID) => {
72
- // Get the creation time of the report.
73
- const reportStats = await getReportStats(timeStamp, jobID);
74
- // If the report does not exist:
75
- if (!reportStats) {
76
- // Log and return this.
77
- console.error(`Report ${timeStamp}-${jobID} does not exist.`);
78
- return {
79
- error: `Report ${timeStamp}-${jobID} could not be retrieved.`
80
- };
81
+ // Accepts an optional precomputed extract to avoid redundant reads when called
82
+ // in a loop over all reports (e.g. by listReports). When the extract comes from
83
+ // getReportExtracts, it carries a superseded flag; otherwise the flag is computed.
84
+ // With an extract provided, failure is impossible, so the return type narrows
85
+ // to ReportBasics; without one, an error object may be returned.
86
+ export function getReportBasics(
87
+ timeStamp: string, jobID: string, extract: ReportExtract
88
+ ): Promise<ReportBasics>;
89
+ export function getReportBasics(
90
+ timeStamp: string, jobID: string, extract?: ReportExtract | null
91
+ ): Promise<ReportBasics | {error: string}>;
92
+ export async function getReportBasics(
93
+ timeStamp: string, jobID: string, extract: ReportExtract | null = null
94
+ ): Promise<ReportBasics | {error: string}> {
95
+ const extractProvided = !!extract;
96
+ // If an extract was not provided, verify the report exists and read it.
97
+ if (!extract) {
98
+ // Get the creation time of the report.
99
+ const reportStats = await getReportStats(timeStamp, jobID);
100
+ // If the report does not exist:
101
+ if (!reportStats) {
102
+ // Log and return this.
103
+ console.error(`Report ${timeStamp}-${jobID} does not exist.`);
104
+ return {
105
+ error: `Report ${timeStamp}-${jobID} could not be retrieved.`
106
+ };
107
+ }
108
+ // Otherwise, i.e. if it exists, get an extract of the report.
109
+ const fetchedExtract = await getReportExtract(timeStamp, jobID);
110
+ // If this failed, return why.
111
+ if ('error' in fetchedExtract) {
112
+ return fetchedExtract;
113
+ }
114
+ extract = fetchedExtract;
81
115
  }
82
- // Otherwise, i.e. if it exists, get an extract of the report.
83
- const reportExtract = await getReportExtract(timeStamp, jobID);
84
- const {url, what} = reportExtract;
85
- const latestReportExtracts = await getLatestReportExtracts();
86
- // Otherwise, i.e. if the report exists, get whether this report has been superseded.
87
- const isSuperseded = latestReportExtracts
88
- .every(extract => extract.timeStamp !== timeStamp || extract.jobID !== jobID);
89
- // Get the completion time from the report content, not the file system birth time,
90
- // because the birth time depends on when the file was created on the file system
91
- // and does not reflect the actual report completion time.
92
- const {reportTime} = reportExtract;
116
+ const {url, description, reportTime} = extract;
117
+ // Get whether this report has been superseded.
118
+ const isSuperseded = extractProvided
119
+ ? extract.superseded === true
120
+ : (await getReportExtracts(true))
121
+ .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
93
122
  // Get the basics about the report.
94
123
  const basics = {
95
124
  identifier: `${timeStamp}-${jobID}`,
96
125
  'completion date and time': reportTime,
97
126
  'days since the report was completed': getAgoDays(new Date(reportTime)),
98
127
  'tested web page': {
99
- description: what,
128
+ description,
100
129
  URL: url
101
130
  },
102
131
  'whether a later report about the same page exists': isSuperseded
103
132
  };
104
133
  // Return them.
105
134
  return basics;
106
- };
135
+ }
107
136
  // Returns the specification of an issue.
108
- exports.getIssueSpec = issueID => {
137
+ export const getIssueSpec = (issueID: string) => {
109
138
  // Get the issue specification.
110
139
  const issueSpec = issueSpecs[issueID];
111
140
  // If it exists:
@@ -122,13 +151,13 @@ exports.getIssueSpec = issueID => {
122
151
  // Otherwise, i.e. if it does not exist, return this.
123
152
  return null;
124
153
  };
125
- // Processes a test or retest request.
126
- exports.processTestRequest = async (testType, what, url, why) => {
154
+ // Processes a test or retest request from the API.
155
+ export const processTestRequest = async (testType: string, description: string, url: string, why: string): Promise<{status: string; message: string} | undefined> => {
127
156
  // Get an email-safe version of the reason.
128
157
  const plainWhy = getPlainText(why);
129
- // Update the waiting recommendations as a transaction.
130
- const updateResult = await updateRecs(what, url, plainWhy);
131
- // If the recommendation was a duplicate:
158
+ // Add the test request as a transaction.
159
+ const updateResult = await addTestRequest(description, url, plainWhy);
160
+ // If the request was a duplicate:
132
161
  if (updateResult.error === 'duplicate') {
133
162
  // Return this.
134
163
  return {
@@ -138,7 +167,7 @@ exports.processTestRequest = async (testType, what, url, why) => {
138
167
  }
139
168
  // Otherwise, i.e. if it was not a duplicate, alert a manager about it.
140
169
  await sendAlert(
141
- `Kilotest: new ${testType} recommendation in the API`,
142
- `Target: ${what}\nURL: ${url}\nReason: ${plainWhy}`
170
+ `Kilotest: new ${testType} request in the API`,
171
+ `Target: ${description}\nURL: ${url}\nReason: ${plainWhy}`
143
172
  );
144
173
  };
package/api/version.ts ADDED
@@ -0,0 +1,8 @@
1
+ /*
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.
4
+ */
5
+
6
+ // CONSTANTS
7
+
8
+ export const version = '2.1.0';
@@ -1,15 +1,14 @@
1
1
  /*
2
- apiIntegration.test.js
2
+ apiIntegration.test.ts
3
3
  Minimal HTTP integration tests verifying routing, body parsing, and JSON serialization, using the fixture corpus and a temporary in-process server.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const http = require('node:http');
12
- const {requestHandler} = require('./index');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import http from 'node:http';
11
+ import {requestHandler} from './index.ts';
13
12
 
14
13
  // CONSTANTS
15
14
 
@@ -18,12 +17,12 @@ const port = 3998;
18
17
  // SETUP AND TEARDOWN
19
18
 
20
19
  const savedDBDir = process.env.DB_DIR;
21
- let server;
20
+ let server: http.Server;
22
21
 
23
22
  before(async () => {
24
- process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
23
+ process.env.DB_DIR = (await import('./test/dbFixture.ts')).fixtureDBDir;
25
24
  server = http.createServer(requestHandler);
26
- await new Promise(resolve => server.listen(port, () => resolve()));
25
+ await new Promise<void>(resolve => server.listen(port, () => resolve()));
27
26
  });
28
27
 
29
28
  after(async () => {
@@ -34,7 +33,7 @@ after(async () => {
34
33
  delete process.env.DB_DIR;
35
34
  }
36
35
  server.closeAllConnections?.();
37
- await new Promise(resolve => {
36
+ await new Promise<void>(resolve => {
38
37
  const timer = setTimeout(() => {
39
38
  server.closeAllConnections?.();
40
39
  resolve();
@@ -49,8 +48,8 @@ after(async () => {
49
48
  // HELPERS
50
49
 
51
50
  // Sends an HTTP request and returns the parsed JSON response body.
52
- const request = (method, requestPath, body = null) => new Promise((resolve, reject) => {
53
- const options = {method, host: 'localhost', port, path: requestPath};
51
+ const request = (method: string, requestPath: string, body: any = null): Promise<any> => new Promise((resolve, reject) => {
52
+ const options: any = {method, host: 'localhost', port, path: requestPath};
54
53
  if (body) {
55
54
  const bodyJSON = JSON.stringify(body);
56
55
  options.headers = {
@@ -59,7 +58,7 @@ const request = (method, requestPath, body = null) => new Promise((resolve, reje
59
58
  };
60
59
  }
61
60
  const req = http.request(options, response => {
62
- const chunks = [];
61
+ const chunks: any[] = [];
63
62
  response.on('data', chunk => chunks.push(chunk));
64
63
  response.on('end', () => {
65
64
  const bodyString = chunks.join('');
@@ -77,7 +76,7 @@ const request = (method, requestPath, body = null) => new Promise((resolve, reje
77
76
 
78
77
  // TESTS
79
78
 
80
- test('GET /api/listReports returns valid JSON through the HTTP routing layer', async () => {
79
+ test('GET /api/listReports returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
81
80
  const {body} = await request('GET', '/api/listReports');
82
81
  assert.equal(body['tool name'], 'listReports');
83
82
  const reports = body['response content']['basics about all available reports'];
package/balances.ts ADDED
@@ -0,0 +1,87 @@
1
+ /*
2
+ balances.ts
3
+ Manages monetary balances and sends alerts when they near exhaustion.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {sendAlert} from './alerts.ts';
9
+ import {errorMessage, getJSON} from './util.ts';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import type {Act, Report} from 'testaro';
13
+
14
+ // CONSTANTS
15
+
16
+ // The path of the file recording the AI service 0 balance.
17
+ export const balancePath = path.join(import.meta.dirname, 'ai0Balance.json');
18
+
19
+ // FUNCTIONS
20
+
21
+ // Checks a report for balances nearing exhaustion.
22
+ export const checkBalancesForAlerts = async (report: Report): Promise<void> => {
23
+ // Get the alert thresholds and prices from the environment.
24
+ const WAVE_THRESHOLD = Number(process.env.WAVE_BALANCE_THRESHOLD);
25
+ const AI_SERVICE0_THRESHOLD = Number(process.env.AI_SERVICE0_BALANCE_THRESHOLD);
26
+ const AI_MODEL0_INPUT_PRICE = Number(process.env.AI_MODEL0_INPUT_PRICE);
27
+ const AI_MODEL0_OUTPUT_PRICE = Number(process.env.AI_MODEL0_OUTPUT_PRICE);
28
+ // If the variables to be monitored for alerts are defined:
29
+ if (WAVE_THRESHOLD && AI_SERVICE0_THRESHOLD && AI_MODEL0_INPUT_PRICE && AI_MODEL0_OUTPUT_PRICE) {
30
+ // WAVE.
31
+ const waveAct = report.acts.find((act: Act) => act.type === 'test' && act.which === 'wave');
32
+ const creditsRemaining = waveAct?.data?.creditsRemaining;
33
+ // If a WAVE balance nearing exhaustion is reported:
34
+ if (typeof creditsRemaining === 'number' && creditsRemaining < WAVE_THRESHOLD) {
35
+ // Alert a manager.
36
+ await sendAlert(
37
+ 'Kilotest: WAVE balance low',
38
+ `Only ${creditsRemaining} WAVE credits remain (3 used per job)`
39
+ );
40
+ }
41
+ const testaroAct = report.acts.find((act: Act) => act.type === 'test' && act.which === 'testaro');
42
+ // Get the AI model token usage for the testaro allCaps test.
43
+ const usage = (testaroAct?.data?.ruleData as {
44
+ allCaps?: {aiModelUsage?: {inputTokens: number; outputTokens: number}}
45
+ } | undefined)?.allCaps?.aiModelUsage;
46
+ let balanceJSON = null;
47
+ try {
48
+ // Get the recorded AI service 0 balance.
49
+ balanceJSON = await fs.readFile(balancePath, 'utf8');
50
+ }
51
+ catch {
52
+ console.error('ERROR: AI service 0 balance file missing');
53
+ }
54
+ // If the variables required for an AI service 0 balance alert are defined:
55
+ if (usage && AI_MODEL0_INPUT_PRICE && AI_MODEL0_OUTPUT_PRICE && balanceJSON) {
56
+ const inputCost = AI_MODEL0_INPUT_PRICE * usage.inputTokens;
57
+ const outputCost = AI_MODEL0_OUTPUT_PRICE * usage.outputTokens;
58
+ const cost = inputCost + outputCost;
59
+ // If any cost was incurred:
60
+ if (cost > 0) {
61
+ // Warn about this.
62
+ console.log(
63
+ 'This job has made the production AI service 0 balance record wrong. Update it.'
64
+ );
65
+ }
66
+ try {
67
+ const balanceData = JSON.parse(balanceJSON);
68
+ // Get an estimate of the balance after this job.
69
+ const newBalance = balanceData.balance - cost;
70
+ // Update the recorded balance.
71
+ await fs.writeFile(balancePath, getJSON({balance: newBalance}));
72
+ console.log(`Estimated new AI Service 0 balance: $${newBalance.toFixed(2)}`);
73
+ // If it is nearing exhaustion:
74
+ if (newBalance < AI_SERVICE0_THRESHOLD) {
75
+ // Alert a manager.
76
+ await sendAlert(
77
+ 'Kilotest: AI service 0 balance low',
78
+ `Balance of AI service 0 account (https://console.anthropic.com) only about $${newBalance.toFixed(2)} (about $0.01 used per job)`
79
+ );
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.log(`ERROR managing AI service 0 balance: ${errorMessage(error)}`);
84
+ }
85
+ }
86
+ }
87
+ };
@@ -1,24 +1,25 @@
1
1
  /*
2
- checkinfo.js
2
+ checkinfo.ts
3
3
  Summarizes the validation scope of the lint, test, and test:smoke scripts by deriving file and test-case counts from their actual configurations, so the summary stays accurate as the codebase changes.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const fs = require('fs');
9
- const path = require('path');
10
- const {execSync} = require('child_process');
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import {execSync} from 'node:child_process';
11
+ import {routes} from './index.ts';
11
12
 
12
13
  // CONSTANTS
13
14
 
14
- const rootDir = __dirname;
15
+ const rootDir = import.meta.dirname;
15
16
  const excludeDirs = new Set(['node_modules', '.git', 'coverage']);
16
17
 
17
18
  // FUNCTIONS
18
19
 
19
20
  // Recursively collects all files under a directory, skipping excluded directories.
20
- const collectFiles = dir => {
21
- const results = [];
21
+ const collectFiles = (dir: string): string[] => {
22
+ const results: string[] = [];
22
23
  for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
23
24
  const fullPath = path.join(dir, entry.name);
24
25
  if (entry.isDirectory()) {
@@ -35,7 +36,7 @@ const collectFiles = dir => {
35
36
 
36
37
  // Converts a glob pattern to a RegExp for matching relative paths.
37
38
  // Supports * (non-segment), ** (any), and literal characters.
38
- const globToRegExp = pattern => {
39
+ const globToRegExp = (pattern: string): RegExp => {
39
40
  const escaped = pattern
40
41
  .replace(/[.+^${}()|[\]\\]/g, '\\$&')
41
42
  .replace(/\*\*/g, '\x00')
@@ -45,33 +46,34 @@ const globToRegExp = pattern => {
45
46
  };
46
47
 
47
48
  // Returns whether a relative path matches any pattern in a list.
48
- const matchesAny = (relPath, patterns) => patterns.some(p => globToRegExp(p).test(relPath));
49
+ const matchesAny = (relPath: string, patterns: string[]) =>
50
+ patterns.some(p => globToRegExp(p).test(relPath));
49
51
 
50
52
  // Extracts the top-level ignores array from eslint.config.mjs.
51
- const getEslintIgnores = () => {
53
+ const getEslintIgnores = (): string[] => {
52
54
  const content = fs.readFileSync(path.join(rootDir, 'eslint.config.mjs'), 'utf8');
53
55
  const match = content.match(/ignores:\s*\[([\s\S]*?)\]/);
54
56
  if (!match) {
55
57
  return [];
56
58
  }
57
- return [...match[1].matchAll(/"([^"]+)"/g)].map(m => m[1]);
59
+ return [...match[1]!.matchAll(/"([^"]+)"/g)].map(m => m[1]!);
58
60
  };
59
61
 
60
62
  // Parses .markdownlint-cli2.jsonc and returns its ignores array.
61
- const getMarkdownlintIgnores = () => {
63
+ const getMarkdownlintIgnores = (): string[] => {
62
64
  const content = fs.readFileSync(path.join(rootDir, '.markdownlint-cli2.jsonc'), 'utf8');
63
65
  const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
64
66
  const match = stripped.match(/"ignores"\s*:\s*\[([\s\S]*?)\]/);
65
67
  if (!match) {
66
68
  return [];
67
69
  }
68
- return [...match[1].matchAll(/"([^"]+)"/g)].map(m => m[1]);
70
+ return [...match[1]!.matchAll(/"([^"]+)"/g)].map(m => m[1]!);
69
71
  };
70
72
 
71
73
  // Runs the Node test runner with the TAP reporter and parses the test count, so dynamically generated tests (e.g., from for-loops) are counted accurately.
72
74
  const getTestCaseCount = () => {
73
75
  const output = execSync(
74
- 'node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=100 --test-reporter=tap',
76
+ "node --require ./test/setup.ts --experimental-test-module-mocks --test --test-concurrency=1 --test-timeout=100 --test-reporter=tap '**/*.test.ts'",
75
77
  {cwd: rootDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']}
76
78
  );
77
79
  const match = output.match(/^# tests (\d+)$/m);
@@ -87,7 +89,7 @@ const eslintIgnores = getEslintIgnores();
87
89
  const markdownlintIgnores = getMarkdownlintIgnores();
88
90
 
89
91
  // ESLint file counts by extension, excluding ignored files.
90
- const eslintExts = {js: 0, json: 0, md: 0, css: 0};
92
+ const eslintExts = {js: 0, ts: 0, json: 0, md: 0, css: 0};
91
93
  for (const rel of relFiles) {
92
94
  if (matchesAny(rel, eslintIgnores)) {
93
95
  continue;
@@ -95,6 +97,9 @@ for (const rel of relFiles) {
95
97
  if (/\.(js|mjs|cjs)$/.test(rel)) {
96
98
  eslintExts.js++;
97
99
  }
100
+ else if (/\.ts$/.test(rel)) {
101
+ eslintExts.ts++;
102
+ }
98
103
  else if (/\.json$/.test(rel)) {
99
104
  eslintExts.json++;
100
105
  }
@@ -113,20 +118,19 @@ const markdownlintCount = relFiles.filter(
113
118
  ).length;
114
119
 
115
120
  // Test file and test-case counts.
116
- const testFiles = relFiles.filter(rel => /\.test\.js$/.test(rel));
121
+ const testFiles = relFiles.filter(rel => /\.test\.ts$/.test(rel));
117
122
  const testCaseCount = getTestCaseCount();
118
123
 
119
- // Smoke-test path counts from the routes table in index.js.
120
- const {routes} = require('./index');
124
+ // Smoke-test path counts from the routes table in index.ts.
121
125
  const smokeGetCount = routes.GET.length;
122
126
  const smokePostCount = routes.POST.length;
123
127
  const smokeTotal = smokeGetCount + smokePostCount;
124
128
 
125
129
  // OUTPUT
126
130
 
127
- const parts = [
131
+ const parts: [string, string[]][] = [
128
132
  ['lint', [
129
- `ESLint: ${eslintTotal} files (${eslintExts.js} JS, ${eslintExts.json} JSON, ${eslintExts.md} MD, ${eslintExts.css} CSS)`,
133
+ `ESLint: ${eslintTotal} files (${eslintExts.js} JS, ${eslintExts.ts} TS, ${eslintExts.json} JSON, ${eslintExts.md} MD, ${eslintExts.css} CSS)`,
130
134
  `markdownlint: ${markdownlintCount} files`
131
135
  ]],
132
136
  ['test', [
package/docs/AI-TOOLS.md CHANGED
@@ -26,14 +26,14 @@ The internal features that make Kilotest a collection of tools for language mode
26
26
  - An `llms.txt` file and an `llms-full.txt` file, documenting the use of Kilotest by language models, conforming to the [llms-txt](https://llmstxt.org/) specification.
27
27
  - A generated `openapi.yaml` file, documenting the Kilotest API, conforming to the [OpenAPI specification](https://spec.openapis.org/oas/v3.1.0).
28
28
  - An `mcp.js` file, providing an MCP server for Kilotest.
29
- - A `server.json` file, conforming to the [MCP server schema](https://modelcontextprotocol.io/), and a GitHub Actions workflow (`publish-mcp.yml`) that authenticates via GitHub OIDC and publishes that file to the official MCP Registry whenever `server.json` or `api/version.js` changes on the `main` branch.
29
+ - A `server.json` file, conforming to the [MCP server schema](https://modelcontextprotocol.io/), and a GitHub Actions workflow (`publish-mcp.yml`) that authenticates via GitHub OIDC and publishes that file to the official MCP Registry whenever `server.json` or `api/version.ts` changes on the `main` branch.
30
30
  - A `capability.md` file with a URL, summarizing what Kilotest tools can do.
31
31
  - `mcp`, `mcp-server`, and `modelcontextprotocol` keywords in `package.json`, supporting discovery of the `@jrpool/kilotest` npm package by tools and humans searching the npm registry for MCP servers.
32
32
  - An `lhm.plugin.json` file, conforming to the [LobeHub plugin manifest format](https://market.lobehub.com/s/publish-mcp/references/manifest), declaring Kilotest as a plugin in the [LobeHub MCP Market](https://lobehub.com/mcp). Publishing and updating are non-automated (`lhm plugin publish`/`update`), because LobeHub requires browser-based OAuth and does not support machine-to-machine publishing.
33
33
  - A `sitemap.xml` file.
34
34
  - Documentation in the `README.md` file.
35
35
  - This `AI-TOOLS.md` file.
36
- - A [tutorial on authorizing language models to use Kilotest](https://kilotest.com/qai/).
36
+ - A [tutorial on authorizing language models to use Kilotest](https://kilotest.com/tutorialAI.html).
37
37
 
38
38
  ## External features
39
39
 
@@ -44,7 +44,7 @@ The external features that support the use of Kilotest as a collection of AI too
44
44
  - A [pull request](https://github.com/w3c/wai-evaluation-tools-list/pull/1153) to add Kilotest to the WAI evaluation tools list.
45
45
  - Registration of Kilotest as an active server in the [official MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=kilotest) (`io.github.jrpool/kilotest`), maintained by the [Model Context Protocol](https://modelcontextprotocol.io/) project. The [PulseMCP](https://www.pulsemcp.com/servers) directory and the VS Code MCP gallery both crawl this registry, so Kilotest is discoverable through them with no separate submission.
46
46
  - Registration of Kilotest with the [Smithery](https://smithery.ai/servers/pool/kilotest) MCP server registry.
47
- - Registration of Kilotest with the [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest) MCP server registry.
47
+ - Registration of Kilotest with the [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest) MCP server registry, including a claim of ownership of the MCP connector by the maintainer via a `TXT` DNS record for `_glama-claim.kilotest.com`, with the value `glama_claim_gVV4HrPdcXr9TEfj1p3icEUBJ3_OVJGX`.
48
48
  - Registration of Kilotest with [Awesome MCP Servers](https://www.tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36).
49
49
  - Registration of Kilotest with the [RapidAPI](https://rapidapi.com/jrpool/api/kilotest/playground/apiendpoint_0f03577a-ff9a-472a-a0ed-533bd198981a) Hub.
50
50
  - Registration of Kilotest with the [LobeHub MCP Market](https://lobehub.com/mcp) (identifier: `jrpool-kilotest`), enabled with an `lhm.plugin.json` file.
@@ -61,7 +61,7 @@ The external features that support the use of Kilotest as a collection of AI too
61
61
 
62
62
  ## Required maintenance
63
63
 
64
- Whenever the `/api/version.js` file is updated:
64
+ Whenever the `/api/version.ts` file is updated:
65
65
 
66
66
  - Update the `server.json` file (version field).
67
67
  - Update the `lhm.plugin.json` file (version field).