@jrpool/kilotest 46.1.2 → 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 +5 -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} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  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} +380 -143
  48. package/{index.js → index.ts} +382 -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 -89
  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 -128
  133. package/util.js +0 -744
  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,15 +72,27 @@ 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
- // Accepts optional precomputed extract and latestReportExtracts to avoid redundant
72
- // reads when called in a loop over all reports (e.g. by listReports).
73
- exports.getReportBasics = async (timeStamp, jobID, extract = null, latestReportExtracts = null) => {
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;
74
96
  // If an extract was not provided, verify the report exists and read it.
75
97
  if (!extract) {
76
98
  // Get the creation time of the report.
@@ -84,32 +106,35 @@ exports.getReportBasics = async (timeStamp, jobID, extract = null, latestReportE
84
106
  };
85
107
  }
86
108
  // Otherwise, i.e. if it exists, get an extract of the report.
87
- extract = await getReportExtract(timeStamp, jobID);
88
- }
89
- const {url, what, reportTime} = extract;
90
- // If the latest report extracts were not provided, compute them.
91
- if (!latestReportExtracts) {
92
- latestReportExtracts = await getLatestReportExtracts();
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;
93
115
  }
116
+ const {url, description, reportTime} = extract;
94
117
  // Get whether this report has been superseded.
95
- const isSuperseded = latestReportExtracts
96
- .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
118
+ const isSuperseded = extractProvided
119
+ ? extract.superseded === true
120
+ : (await getReportExtracts(true))
121
+ .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
97
122
  // Get the basics about the report.
98
123
  const basics = {
99
124
  identifier: `${timeStamp}-${jobID}`,
100
125
  'completion date and time': reportTime,
101
126
  'days since the report was completed': getAgoDays(new Date(reportTime)),
102
127
  'tested web page': {
103
- description: what,
128
+ description,
104
129
  URL: url
105
130
  },
106
131
  'whether a later report about the same page exists': isSuperseded
107
132
  };
108
133
  // Return them.
109
134
  return basics;
110
- };
135
+ }
111
136
  // Returns the specification of an issue.
112
- exports.getIssueSpec = issueID => {
137
+ export const getIssueSpec = (issueID: string) => {
113
138
  // Get the issue specification.
114
139
  const issueSpec = issueSpecs[issueID];
115
140
  // If it exists:
@@ -126,13 +151,13 @@ exports.getIssueSpec = issueID => {
126
151
  // Otherwise, i.e. if it does not exist, return this.
127
152
  return null;
128
153
  };
129
- // Processes a test or retest request.
130
- 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> => {
131
156
  // Get an email-safe version of the reason.
132
157
  const plainWhy = getPlainText(why);
133
- // Update the waiting recommendations as a transaction.
134
- const updateResult = await updateRecs(what, url, plainWhy);
135
- // 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:
136
161
  if (updateResult.error === 'duplicate') {
137
162
  // Return this.
138
163
  return {
@@ -142,7 +167,7 @@ exports.processTestRequest = async (testType, what, url, why) => {
142
167
  }
143
168
  // Otherwise, i.e. if it was not a duplicate, alert a manager about it.
144
169
  await sendAlert(
145
- `Kilotest: new ${testType} recommendation in the API`,
146
- `Target: ${what}\nURL: ${url}\nReason: ${plainWhy}`
170
+ `Kilotest: new ${testType} request in the API`,
171
+ `Target: ${description}\nURL: ${url}\nReason: ${plainWhy}`
147
172
  );
148
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('');
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).