x-readiness-mcp 0.3.0 → 0.5.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.
- package/README.md +325 -0
- package/USAGE_GUIDE.md +271 -0
- package/package.json +2 -2
- package/server.js +103 -213
- package/tools/autofix.js +151 -0
- package/tools/execution.js +193 -814
- package/tools/gitignore-helper.js +88 -0
- package/tools/planning.js +120 -333
- package/tools/rule-checkers.js +394 -0
- package/tools/template-parser.js +204 -0
- package/templates/master_template.json +0 -524
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// mcp/tools/rule-checkers.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Recursively find all source files in a repository
|
|
8
|
+
*/
|
|
9
|
+
async function findSourceFiles(repoPath, extensions = ["js", "ts", "json", "yaml", "yml"]) {
|
|
10
|
+
const patterns = extensions.map(ext => `**/*.${ext}`);
|
|
11
|
+
const allFiles = [];
|
|
12
|
+
|
|
13
|
+
for (const pattern of patterns) {
|
|
14
|
+
const files = await glob(pattern, {
|
|
15
|
+
cwd: repoPath,
|
|
16
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.git/**", "**/mcp/**"],
|
|
17
|
+
absolute: true
|
|
18
|
+
});
|
|
19
|
+
allFiles.push(...files);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return allFiles;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read file content with line tracking
|
|
27
|
+
*/
|
|
28
|
+
async function readFileLines(filePath) {
|
|
29
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
return { content, lines, filePath };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Base checker function - to be extended for specific rules
|
|
36
|
+
*/
|
|
37
|
+
async function checkRule(repoPath, rule) {
|
|
38
|
+
const ruleId = rule.rule_id.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Route to specific checker based on rule category
|
|
41
|
+
const category = rule.category.toLowerCase();
|
|
42
|
+
|
|
43
|
+
if (category.includes("pagination")) {
|
|
44
|
+
return await checkPaginationRule(repoPath, rule);
|
|
45
|
+
} else if (category.includes("error")) {
|
|
46
|
+
return await checkErrorHandlingRule(repoPath, rule);
|
|
47
|
+
} else if (category.includes("naming")) {
|
|
48
|
+
return await checkNamingRule(repoPath, rule);
|
|
49
|
+
} else if (category.includes("security")) {
|
|
50
|
+
return await checkSecurityRule(repoPath, rule);
|
|
51
|
+
} else if (category.includes("versioning")) {
|
|
52
|
+
return await checkVersioningRule(repoPath, rule);
|
|
53
|
+
} else if (category.includes("media")) {
|
|
54
|
+
return await checkMediaTypesRule(repoPath, rule);
|
|
55
|
+
} else if (category.includes("operations")) {
|
|
56
|
+
return await checkOperationsRule(repoPath, rule);
|
|
57
|
+
} else if (category.includes("filtering")) {
|
|
58
|
+
return await checkFilteringRule(repoPath, rule);
|
|
59
|
+
} else if (category.includes("sorting")) {
|
|
60
|
+
return await checkSortingRule(repoPath, rule);
|
|
61
|
+
} else if (category.includes("sparse")) {
|
|
62
|
+
return await checkSparseFieldsetsRule(repoPath, rule);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Default: not yet implemented
|
|
66
|
+
return {
|
|
67
|
+
status: "SKIPPED",
|
|
68
|
+
reason: "Checker not implemented for this rule category",
|
|
69
|
+
violations: []
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check pagination rules
|
|
75
|
+
*/
|
|
76
|
+
async function checkPaginationRule(repoPath, rule) {
|
|
77
|
+
const violations = [];
|
|
78
|
+
const files = await findSourceFiles(repoPath, ["js", "ts"]);
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const { lines } = await readFileLines(file);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < lines.length; i++) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
const lineNum = i + 1;
|
|
86
|
+
|
|
87
|
+
// Check for pagination-related patterns
|
|
88
|
+
if (rule.rule_id === "600") {
|
|
89
|
+
// Check if pagination links are provided
|
|
90
|
+
if (line.includes("pagination") || line.includes("/page")) {
|
|
91
|
+
// Look for links object
|
|
92
|
+
const contextLines = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join("\n");
|
|
93
|
+
if (!contextLines.includes("links") && !contextLines.includes("next") && !contextLines.includes("prev")) {
|
|
94
|
+
violations.push({
|
|
95
|
+
file_path: path.relative(repoPath, file),
|
|
96
|
+
line_number: lineNum,
|
|
97
|
+
line_content: line.trim(),
|
|
98
|
+
message: "Pagination endpoint should include links for navigation (next, prev, etc.)"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check for cursor/offset/index pagination patterns
|
|
105
|
+
if (rule.rule_id.startsWith("601")) {
|
|
106
|
+
if (line.includes("page") && (line.includes("?") || line.includes("query"))) {
|
|
107
|
+
const contextLines = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 5)).join("\n");
|
|
108
|
+
|
|
109
|
+
if (rule.rule_id.includes("601.1") && !contextLines.match(/cursor|token/i)) {
|
|
110
|
+
violations.push({
|
|
111
|
+
file_path: path.relative(repoPath, file),
|
|
112
|
+
line_number: lineNum,
|
|
113
|
+
line_content: line.trim(),
|
|
114
|
+
message: "Consider using cursor-based pagination strategy"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (rule.rule_id.includes("601.2") && !contextLines.match(/offset|limit/i)) {
|
|
119
|
+
violations.push({
|
|
120
|
+
file_path: path.relative(repoPath, file),
|
|
121
|
+
line_number: lineNum,
|
|
122
|
+
line_content: line.trim(),
|
|
123
|
+
message: "Offset-based pagination should include offset and limit parameters"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations.length > 0
|
|
132
|
+
? { status: "FAIL", violations }
|
|
133
|
+
: { status: "PASS", violations: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check error handling rules
|
|
138
|
+
*/
|
|
139
|
+
async function checkErrorHandlingRule(repoPath, rule) {
|
|
140
|
+
const violations = [];
|
|
141
|
+
const files = await findSourceFiles(repoPath, ["js", "ts"]);
|
|
142
|
+
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const { lines } = await readFileLines(file);
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i];
|
|
148
|
+
const lineNum = i + 1;
|
|
149
|
+
|
|
150
|
+
// Rule 300: Check for proper HTTP status codes
|
|
151
|
+
if (rule.rule_id === "300") {
|
|
152
|
+
if (line.match(/\.status\((\d+)\)|\.sendStatus\((\d+)\)|statusCode\s*=\s*(\d+)/)) {
|
|
153
|
+
const statusMatch = line.match(/(\d{3})/);
|
|
154
|
+
if (statusMatch) {
|
|
155
|
+
const status = parseInt(statusMatch[1]);
|
|
156
|
+
// Check if status code is valid HTTP status
|
|
157
|
+
if (status < 100 || status > 599) {
|
|
158
|
+
violations.push({
|
|
159
|
+
file_path: path.relative(repoPath, file),
|
|
160
|
+
line_number: lineNum,
|
|
161
|
+
line_content: line.trim(),
|
|
162
|
+
message: `Invalid HTTP status code ${status}. Use official HTTP status codes (100-599)`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Rule 303: Check for stack traces in error responses
|
|
170
|
+
if (rule.rule_id === "303") {
|
|
171
|
+
if (line.match(/stack|stackTrace/i) && (line.includes("response") || line.includes("send") || line.includes("json"))) {
|
|
172
|
+
violations.push({
|
|
173
|
+
file_path: path.relative(repoPath, file),
|
|
174
|
+
line_number: lineNum,
|
|
175
|
+
line_content: line.trim(),
|
|
176
|
+
message: "API should not expose stack traces in error responses (security risk)"
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Rule 304: Check for errors object in error responses
|
|
182
|
+
if (rule.rule_id === "304") {
|
|
183
|
+
if (line.match(/catch|error/i) && line.match(/\.json\(|\.send\(/)) {
|
|
184
|
+
const contextLines = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join("\n");
|
|
185
|
+
if (!contextLines.includes("errors")) {
|
|
186
|
+
violations.push({
|
|
187
|
+
file_path: path.relative(repoPath, file),
|
|
188
|
+
line_number: lineNum,
|
|
189
|
+
line_content: line.trim(),
|
|
190
|
+
message: "Error response should include an 'errors' object array"
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return violations.length > 0
|
|
199
|
+
? { status: "FAIL", violations }
|
|
200
|
+
: { status: "PASS", violations: [] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check naming convention rules
|
|
205
|
+
*/
|
|
206
|
+
async function checkNamingRule(repoPath, rule) {
|
|
207
|
+
const violations = [];
|
|
208
|
+
const files = await findSourceFiles(repoPath, ["js", "ts", "json"]);
|
|
209
|
+
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const { content, lines } = await readFileLines(file);
|
|
212
|
+
|
|
213
|
+
// For JSON files, check property names
|
|
214
|
+
if (file.endsWith(".json")) {
|
|
215
|
+
try {
|
|
216
|
+
const json = JSON.parse(content);
|
|
217
|
+
checkObjectNaming(json, file, violations, repoPath, rule);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Skip invalid JSON
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// For JS/TS files, check object property definitions
|
|
223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
224
|
+
const line = lines[i];
|
|
225
|
+
const lineNum = i + 1;
|
|
226
|
+
|
|
227
|
+
// Check for object property definitions
|
|
228
|
+
const propMatch = line.match(/['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]:/);
|
|
229
|
+
if (propMatch) {
|
|
230
|
+
const propName = propMatch[1];
|
|
231
|
+
|
|
232
|
+
// Rule naming_01: Must start with lowercase
|
|
233
|
+
if (rule.rule_id === "naming_01" && /^[A-Z]/.test(propName) && propName !== propName.toUpperCase()) {
|
|
234
|
+
violations.push({
|
|
235
|
+
file_path: path.relative(repoPath, file),
|
|
236
|
+
line_number: lineNum,
|
|
237
|
+
line_content: line.trim(),
|
|
238
|
+
message: `Property "${propName}" should start with a lowercase letter (camelCase)`
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Rule naming_02: Must use camelCase
|
|
243
|
+
if (rule.rule_id === "naming_02" && propName.includes("_") && !propName.startsWith("_")) {
|
|
244
|
+
violations.push({
|
|
245
|
+
file_path: path.relative(repoPath, file),
|
|
246
|
+
line_number: lineNum,
|
|
247
|
+
line_content: line.trim(),
|
|
248
|
+
message: `Property "${propName}" should use camelCase instead of snake_case`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Rule naming_04: Must use ASCII characters only
|
|
253
|
+
if (rule.rule_id === "naming_04" && /[^\x00-\x7F]/.test(propName)) {
|
|
254
|
+
violations.push({
|
|
255
|
+
file_path: path.relative(repoPath, file),
|
|
256
|
+
line_number: lineNum,
|
|
257
|
+
line_content: line.trim(),
|
|
258
|
+
message: `Property "${propName}" contains non-ASCII characters`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return violations.length > 0
|
|
267
|
+
? { status: "FAIL", violations }
|
|
268
|
+
: { status: "PASS", violations: [] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Helper to check object naming recursively
|
|
273
|
+
*/
|
|
274
|
+
function checkObjectNaming(obj, filePath, violations, repoPath, rule, path = "") {
|
|
275
|
+
if (typeof obj !== "object" || obj === null) return;
|
|
276
|
+
|
|
277
|
+
for (const key in obj) {
|
|
278
|
+
const fullPath = path ? `${path}.${key}` : key;
|
|
279
|
+
|
|
280
|
+
if (rule.rule_id === "naming_01" && /^[A-Z]/.test(key) && key !== key.toUpperCase()) {
|
|
281
|
+
violations.push({
|
|
282
|
+
file_path: path.relative(repoPath, filePath),
|
|
283
|
+
line_number: 0,
|
|
284
|
+
line_content: `"${key}": ...`,
|
|
285
|
+
message: `JSON property "${fullPath}" should start with lowercase (camelCase)`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (rule.rule_id === "naming_02" && key.includes("_") && !key.startsWith("_")) {
|
|
290
|
+
violations.push({
|
|
291
|
+
file_path: path.relative(repoPath, filePath),
|
|
292
|
+
line_number: 0,
|
|
293
|
+
line_content: `"${key}": ...`,
|
|
294
|
+
message: `JSON property "${fullPath}" should use camelCase instead of snake_case`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (Array.isArray(obj[key])) {
|
|
299
|
+
obj[key].forEach((item, idx) => checkObjectNaming(item, filePath, violations, repoPath, rule, `${fullPath}[${idx}]`));
|
|
300
|
+
} else if (typeof obj[key] === "object") {
|
|
301
|
+
checkObjectNaming(obj[key], filePath, violations, repoPath, rule, fullPath);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check security rules
|
|
308
|
+
*/
|
|
309
|
+
async function checkSecurityRule(repoPath, rule) {
|
|
310
|
+
const violations = [];
|
|
311
|
+
const files = await findSourceFiles(repoPath, ["js", "ts"]);
|
|
312
|
+
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
const { lines } = await readFileLines(file);
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < lines.length; i++) {
|
|
317
|
+
const line = lines[i];
|
|
318
|
+
const lineNum = i + 1;
|
|
319
|
+
|
|
320
|
+
// Rule sec_01: OAuth 2.0 Bearer Token
|
|
321
|
+
if (rule.rule_id === "sec_01" || rule.rule_id === "sec_09") {
|
|
322
|
+
if (line.match(/app\.(get|post|put|delete|patch)/i) || line.match(/router\.(get|post|put|delete|patch)/i)) {
|
|
323
|
+
const contextLines = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 10)).join("\n");
|
|
324
|
+
if (!contextLines.match(/authorization|bearer|token|auth/i)) {
|
|
325
|
+
violations.push({
|
|
326
|
+
file_path: path.relative(repoPath, file),
|
|
327
|
+
line_number: lineNum,
|
|
328
|
+
line_content: line.trim(),
|
|
329
|
+
message: "API endpoint should be secured with OAuth 2.0 Bearer Token authentication"
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Rule sec_02: TLS/HTTPS
|
|
336
|
+
if (rule.rule_id === "sec_02") {
|
|
337
|
+
if (line.match(/http:\/\//) && !line.includes("localhost") && !line.includes("127.0.0.1")) {
|
|
338
|
+
violations.push({
|
|
339
|
+
file_path: path.relative(repoPath, file),
|
|
340
|
+
line_number: lineNum,
|
|
341
|
+
line_content: line.trim(),
|
|
342
|
+
message: "Use HTTPS instead of HTTP for API protection"
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return violations.length > 0
|
|
350
|
+
? { status: "FAIL", violations }
|
|
351
|
+
: { status: "PASS", violations: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Stub checkers for other categories
|
|
356
|
+
*/
|
|
357
|
+
async function checkVersioningRule(repoPath, rule) {
|
|
358
|
+
return { status: "SKIPPED", reason: "Versioning checker not yet implemented", violations: [] };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function checkMediaTypesRule(repoPath, rule) {
|
|
362
|
+
return { status: "SKIPPED", reason: "Media types checker not yet implemented", violations: [] };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function checkOperationsRule(repoPath, rule) {
|
|
366
|
+
return { status: "SKIPPED", reason: "Operations checker not yet implemented", violations: [] };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function checkFilteringRule(repoPath, rule) {
|
|
370
|
+
return { status: "SKIPPED", reason: "Filtering checker not yet implemented", violations: [] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function checkSortingRule(repoPath, rule) {
|
|
374
|
+
return { status: "SKIPPED", reason: "Sorting checker not yet implemented", violations: [] };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function checkSparseFieldsetsRule(repoPath, rule) {
|
|
378
|
+
return { status: "SKIPPED", reason: "Sparse fieldsets checker not yet implemented", violations: [] };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Main export - evaluate a single rule
|
|
383
|
+
*/
|
|
384
|
+
export async function evaluateRule(repoPath, rule) {
|
|
385
|
+
try {
|
|
386
|
+
return await checkRule(repoPath, rule);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return {
|
|
389
|
+
status: "ERROR",
|
|
390
|
+
reason: error.message,
|
|
391
|
+
violations: []
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// mcp/tools/template-parser.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TEMPLATE_PATH = path.resolve(process.cwd(), "mcp", "templates", "master_template.md");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse the master template to extract scopes, categories, and rules
|
|
9
|
+
* @returns {Promise<Object>} Parsed checklist with scopes and rules
|
|
10
|
+
*/
|
|
11
|
+
export async function parseTemplate() {
|
|
12
|
+
const content = await fs.readFile(TEMPLATE_PATH, "utf8");
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
|
|
15
|
+
const result = {
|
|
16
|
+
template_version: "2.5.0",
|
|
17
|
+
last_updated: "2026-01-28",
|
|
18
|
+
scopes: [],
|
|
19
|
+
categories: [],
|
|
20
|
+
reference_urls: {
|
|
21
|
+
common: "https://developer.siemens.com/guidelines/api-guidelines/common/index.html",
|
|
22
|
+
rest: "https://developer.siemens.com/guidelines/api-guidelines/rest/index.html"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let currentSection = null;
|
|
27
|
+
let currentScope = null;
|
|
28
|
+
let currentCategory = null;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < lines.length; i++) {
|
|
31
|
+
const line = lines[i].trim();
|
|
32
|
+
|
|
33
|
+
// Detect sections
|
|
34
|
+
if (line === "## Scopes") {
|
|
35
|
+
currentSection = "scopes";
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (line === "## Rule Categories") {
|
|
39
|
+
currentSection = "categories";
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse scopes
|
|
44
|
+
if (currentSection === "scopes" && line.startsWith("### ")) {
|
|
45
|
+
const scopeName = line.substring(4).trim();
|
|
46
|
+
currentScope = {
|
|
47
|
+
name: scopeName,
|
|
48
|
+
description: "",
|
|
49
|
+
includes: []
|
|
50
|
+
};
|
|
51
|
+
result.scopes.push(currentScope);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentScope && line.startsWith("**Description:**")) {
|
|
56
|
+
currentScope.description = line.substring(16).trim();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (currentScope && line.startsWith("- ")) {
|
|
61
|
+
const include = line.substring(2).trim();
|
|
62
|
+
if (include) currentScope.includes.push(include);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse categories
|
|
67
|
+
if (currentSection === "categories" && line.match(/^### \d+\./)) {
|
|
68
|
+
const match = line.match(/^### \d+\.\s+(.+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
const categoryName = match[1].trim();
|
|
71
|
+
currentCategory = {
|
|
72
|
+
name: categoryName,
|
|
73
|
+
priority: "",
|
|
74
|
+
guideline_url: "",
|
|
75
|
+
rules: []
|
|
76
|
+
};
|
|
77
|
+
result.categories.push(currentCategory);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentCategory && line.startsWith("**Priority:**")) {
|
|
83
|
+
currentCategory.priority = line.substring(13).trim();
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (currentCategory && line.startsWith("**Guideline:**")) {
|
|
88
|
+
// Extract URLs from markdown links
|
|
89
|
+
const urlMatches = line.match(/\[([^\]]+)\]\(([^)]+)\)/g);
|
|
90
|
+
if (urlMatches && urlMatches.length > 0) {
|
|
91
|
+
const firstUrl = urlMatches[0].match(/\(([^)]+)\)/);
|
|
92
|
+
if (firstUrl) {
|
|
93
|
+
currentCategory.guideline_url = firstUrl[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentCategory && line.startsWith("- ")) {
|
|
100
|
+
const ruleMatch = line.match(/^- ([^:]+):\s*(.+)/);
|
|
101
|
+
if (ruleMatch) {
|
|
102
|
+
const ruleId = ruleMatch[1].trim();
|
|
103
|
+
const ruleDescription = ruleMatch[2].trim();
|
|
104
|
+
currentCategory.rules.push({
|
|
105
|
+
rule_id: ruleId,
|
|
106
|
+
rule_name: ruleDescription,
|
|
107
|
+
category: currentCategory.name,
|
|
108
|
+
priority: currentCategory.priority,
|
|
109
|
+
guideline_url: currentCategory.guideline_url
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reset scope when we hit a separator
|
|
116
|
+
if (line === "---") {
|
|
117
|
+
currentScope = null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate a checklist based on scope and developer prompt
|
|
126
|
+
* @param {string} scope - The scope to use (e.g., 'x_api_readiness', 'pagination_ready')
|
|
127
|
+
* @param {string} developerPrompt - Developer intent to filter rules
|
|
128
|
+
* @returns {Promise<Object>} Filtered checklist
|
|
129
|
+
*/
|
|
130
|
+
export async function generateChecklist(scope, developerPrompt = "") {
|
|
131
|
+
const template = await parseTemplate();
|
|
132
|
+
|
|
133
|
+
// Find the requested scope
|
|
134
|
+
const scopeConfig = template.scopes.find(s => s.name === scope);
|
|
135
|
+
if (!scopeConfig) {
|
|
136
|
+
throw new Error(`Unknown scope: ${scope}. Available scopes: ${template.scopes.map(s => s.name).join(", ")}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Filter categories based on scope includes
|
|
140
|
+
let selectedCategories = [];
|
|
141
|
+
|
|
142
|
+
if (scope === "x_api_readiness") {
|
|
143
|
+
// Include all categories for full X-API readiness
|
|
144
|
+
selectedCategories = template.categories;
|
|
145
|
+
} else {
|
|
146
|
+
// Filter by scope's includes
|
|
147
|
+
selectedCategories = template.categories.filter(cat => {
|
|
148
|
+
return scopeConfig.includes.some(inc =>
|
|
149
|
+
cat.name.toLowerCase().includes(inc.toLowerCase()) ||
|
|
150
|
+
inc.toLowerCase().includes(cat.name.toLowerCase())
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Further filter by developer prompt (keyword matching)
|
|
156
|
+
const prompt = developerPrompt.toLowerCase();
|
|
157
|
+
if (prompt && scope !== "x_api_readiness") {
|
|
158
|
+
const keywords = extractKeywords(prompt);
|
|
159
|
+
if (keywords.length > 0) {
|
|
160
|
+
selectedCategories = selectedCategories.filter(cat => {
|
|
161
|
+
const catName = cat.name.toLowerCase();
|
|
162
|
+
return keywords.some(kw => catName.includes(kw));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
template_version: template.template_version,
|
|
169
|
+
last_updated: template.last_updated,
|
|
170
|
+
scope: scope,
|
|
171
|
+
scope_description: scopeConfig.description,
|
|
172
|
+
developer_prompt: developerPrompt,
|
|
173
|
+
reference_urls: template.reference_urls,
|
|
174
|
+
categories: selectedCategories,
|
|
175
|
+
total_rules: selectedCategories.reduce((sum, cat) => sum + cat.rules.length, 0)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract keywords from developer prompt for filtering
|
|
181
|
+
*/
|
|
182
|
+
function extractKeywords(prompt) {
|
|
183
|
+
const keywordMap = {
|
|
184
|
+
"pagination": ["pagination", "paging", "page"],
|
|
185
|
+
"error": ["error", "errors", "error handling"],
|
|
186
|
+
"security": ["security", "auth", "oauth", "jwt", "token"],
|
|
187
|
+
"naming": ["naming", "camelcase", "kebab-case"],
|
|
188
|
+
"versioning": ["version", "versioning"],
|
|
189
|
+
"media": ["media", "content type", "json"],
|
|
190
|
+
"sorting": ["sort", "sorting"],
|
|
191
|
+
"filtering": ["filter", "filtering"],
|
|
192
|
+
"bulk": ["bulk", "batch"],
|
|
193
|
+
"sparse": ["sparse", "fields"],
|
|
194
|
+
"operations": ["operations", "crud"]
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const found = [];
|
|
198
|
+
for (const [category, keywords] of Object.entries(keywordMap)) {
|
|
199
|
+
if (keywords.some(kw => prompt.includes(kw))) {
|
|
200
|
+
found.push(category);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return found;
|
|
204
|
+
}
|