x-readiness-mcp 0.3.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/bin/cli.js +3 -0
- package/package.json +27 -0
- package/server.js +284 -0
- package/templates/master_template.json +524 -0
- package/templates/master_template.md +374 -0
- package/tools/execution.js +852 -0
- package/tools/planning.js +343 -0
- package/x-readiness-mcp-0.3.0.tgz +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// tools/planning.js
|
|
2
|
+
// X-API Readiness Planning Tool (Deterministic / Offline)
|
|
3
|
+
//
|
|
4
|
+
// Contract:
|
|
5
|
+
// - Input: { scope, format }
|
|
6
|
+
// - Output: canonical JSON with `features[]` (stable IDs), optionally `markdown`.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT: No network calls or HTML scraping. Everything is generated from
|
|
9
|
+
// `templates/master_template.json` (sources/scopes/categories.expected_rules).
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const MASTER_TEMPLATE_PATH = path.join(__dirname, "..", "templates", "master_template.json");
|
|
19
|
+
const MASTER_TEMPLATE_MD_PATH = path.join(__dirname, "..", "templates", "master_template.md");
|
|
20
|
+
|
|
21
|
+
export async function generateChecklist(scope, format = "json") {
|
|
22
|
+
// Try to load JSON first, fallback to parsing MD if JSON doesn't exist
|
|
23
|
+
let template;
|
|
24
|
+
try {
|
|
25
|
+
template = loadMasterTemplate();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// If JSON doesn't exist, we'll use the MD version (for now just throw)
|
|
28
|
+
throw new Error(`Master template not found. Please ensure master_template.json exists: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
const scopeKey = resolveScopeKey(scope, template);
|
|
31
|
+
const selectedSources = selectSourcesByScope(scopeKey, template);
|
|
32
|
+
const selectedCategories = selectCategoriesForSources(selectedSources, template);
|
|
33
|
+
|
|
34
|
+
const checklist = buildChecklistFromTemplate(scopeKey, selectedCategories, selectedSources, template);
|
|
35
|
+
|
|
36
|
+
if (format === "markdown") {
|
|
37
|
+
return {
|
|
38
|
+
...checklist,
|
|
39
|
+
markdown: formatAsMarkdown(checklist)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (format === "both") {
|
|
44
|
+
return {
|
|
45
|
+
...checklist,
|
|
46
|
+
markdown: formatAsMarkdown(checklist)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return checklist;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const planningTool = generateChecklist;
|
|
54
|
+
|
|
55
|
+
function loadMasterTemplate() {
|
|
56
|
+
const raw = fs.readFileSync(MASTER_TEMPLATE_PATH, "utf8");
|
|
57
|
+
const template = JSON.parse(raw);
|
|
58
|
+
|
|
59
|
+
if (!template || typeof template !== "object") {
|
|
60
|
+
throw new Error("Invalid master template: not an object");
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(template.sources)) {
|
|
63
|
+
throw new Error("Invalid master template: missing 'sources[]'");
|
|
64
|
+
}
|
|
65
|
+
if (!template.scopes || typeof template.scopes !== "object") {
|
|
66
|
+
throw new Error("Invalid master template: missing 'scopes'");
|
|
67
|
+
}
|
|
68
|
+
if (!template.categories || typeof template.categories !== "object") {
|
|
69
|
+
throw new Error("Invalid master template: missing 'categories'");
|
|
70
|
+
}
|
|
71
|
+
return template;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveScopeKey(scope, template) {
|
|
75
|
+
const scopeInput = (scope || "").toString().trim();
|
|
76
|
+
if (!scopeInput) return "x_api_readiness";
|
|
77
|
+
|
|
78
|
+
const directKey = scopeInput.toLowerCase();
|
|
79
|
+
if (template.scopes[directKey]) return directKey;
|
|
80
|
+
|
|
81
|
+
const normalized = directKey.replace(/[_-]/g, "");
|
|
82
|
+
for (const key of Object.keys(template.scopes)) {
|
|
83
|
+
if (key.replace(/[_-]/g, "") === normalized) return key;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return "x_api_readiness";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function selectSourcesByScope(scopeKey, template) {
|
|
90
|
+
const scopeConfig = template.scopes[scopeKey] || template.scopes.x_api_readiness;
|
|
91
|
+
const includePatterns = Array.isArray(scopeConfig?.include_ids) ? scopeConfig.include_ids : ["common-*", "rest-*"];
|
|
92
|
+
const excludePatterns = Array.isArray(scopeConfig?.exclude_ids) ? scopeConfig.exclude_ids : [];
|
|
93
|
+
|
|
94
|
+
return template.sources.filter((source) => {
|
|
95
|
+
if (!source || !source.id) return false;
|
|
96
|
+
|
|
97
|
+
const isIncluded = includePatterns.some((pattern) => matchIdPattern(source.id, pattern));
|
|
98
|
+
if (!isIncluded) return false;
|
|
99
|
+
|
|
100
|
+
const isExcluded = excludePatterns.some((pattern) => matchIdPattern(source.id, pattern));
|
|
101
|
+
if (isExcluded) return false;
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function matchIdPattern(id, pattern) {
|
|
108
|
+
if (!pattern) return false;
|
|
109
|
+
if (pattern === "*") return true;
|
|
110
|
+
if (pattern.endsWith("*")) return id.startsWith(pattern.slice(0, -1));
|
|
111
|
+
if (pattern.startsWith("*")) return id.endsWith(pattern.slice(1));
|
|
112
|
+
return id === pattern;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function selectCategoriesForSources(selectedSources, template) {
|
|
116
|
+
const categories = template.categories;
|
|
117
|
+
|
|
118
|
+
// Build a reverse lookup: normalized guideline URL -> [{ categoryKey, category }]
|
|
119
|
+
const byUrl = new Map();
|
|
120
|
+
for (const [categoryKey, category] of Object.entries(categories)) {
|
|
121
|
+
const url = normalizeUrl(category?.guideline_url);
|
|
122
|
+
if (!url) continue;
|
|
123
|
+
const existing = byUrl.get(url) || [];
|
|
124
|
+
existing.push({ categoryKey, category });
|
|
125
|
+
byUrl.set(url, existing);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const selected = new Map();
|
|
129
|
+
for (const source of selectedSources) {
|
|
130
|
+
const url = normalizeUrl(source?.url);
|
|
131
|
+
if (!url) continue;
|
|
132
|
+
const matches = byUrl.get(url);
|
|
133
|
+
if (!matches || matches.length === 0) continue;
|
|
134
|
+
|
|
135
|
+
for (const match of matches) {
|
|
136
|
+
selected.set(match.categoryKey, match.category);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return selected;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeUrl(url) {
|
|
144
|
+
if (!url || typeof url !== "string") return null;
|
|
145
|
+
try {
|
|
146
|
+
const parsed = new URL(url);
|
|
147
|
+
parsed.hash = "";
|
|
148
|
+
// Some URLs might differ only by trailing slash
|
|
149
|
+
const normalized = parsed.toString().replace(/\/$/, "");
|
|
150
|
+
return normalized;
|
|
151
|
+
} catch {
|
|
152
|
+
return url.replace(/#.*$/, "").replace(/\/$/, "");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildChecklistFromTemplate(scopeKey, selectedCategoriesMap, selectedSources, template) {
|
|
157
|
+
const generatedAt = new Date().toISOString();
|
|
158
|
+
const checklistId = `xreadiness:${template.version || "0"}:${scopeKey}`;
|
|
159
|
+
|
|
160
|
+
const selectedCategories = Array.from(selectedCategoriesMap.entries())
|
|
161
|
+
.map(([categoryKey, category]) => ({ categoryKey, category }))
|
|
162
|
+
.sort((a, b) => a.categoryKey.localeCompare(b.categoryKey));
|
|
163
|
+
|
|
164
|
+
const features = selectedCategories
|
|
165
|
+
.map(({ categoryKey, category }) => buildFeature(categoryKey, category))
|
|
166
|
+
.filter((f) => f.rules.length > 0);
|
|
167
|
+
|
|
168
|
+
// Derived flattened rules for execution tool compatibility
|
|
169
|
+
const rules = [];
|
|
170
|
+
for (const feature of features) {
|
|
171
|
+
for (const rule of feature.rules) {
|
|
172
|
+
rules.push({
|
|
173
|
+
ruleId: rule.rule_id,
|
|
174
|
+
ruleName: rule.rule_name,
|
|
175
|
+
category: feature.category_key,
|
|
176
|
+
categoryKey: feature.category_key,
|
|
177
|
+
normative: rule.normative,
|
|
178
|
+
severity: rule.severity,
|
|
179
|
+
referenceUrl: rule.reference_url
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const summary = computeSummary(features);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
checklist_id: checklistId,
|
|
188
|
+
scope: scopeKey,
|
|
189
|
+
generated_at: generatedAt,
|
|
190
|
+
template_version: template.version || null,
|
|
191
|
+
template_last_updated: template.last_updated || null,
|
|
192
|
+
|
|
193
|
+
// Canonical contract
|
|
194
|
+
features,
|
|
195
|
+
|
|
196
|
+
// Derived compatibility field for current execution tool
|
|
197
|
+
rules,
|
|
198
|
+
|
|
199
|
+
// Supporting metadata
|
|
200
|
+
sources_included: selectedSources.map((s) => ({
|
|
201
|
+
id: s.id,
|
|
202
|
+
area: s.area,
|
|
203
|
+
name: s.name,
|
|
204
|
+
url: s.url,
|
|
205
|
+
priority: s.priority || null
|
|
206
|
+
})),
|
|
207
|
+
summary
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildFeature(categoryKey, category) {
|
|
212
|
+
const featureId = category?.id || categoryKey.toLowerCase();
|
|
213
|
+
const featureName = category?.name || categoryKey;
|
|
214
|
+
const guidelineUrl = category?.guideline_url || null;
|
|
215
|
+
const priority = category?.priority || "medium";
|
|
216
|
+
|
|
217
|
+
const expectedRules = category?.expected_rules && typeof category.expected_rules === "object"
|
|
218
|
+
? category.expected_rules
|
|
219
|
+
: {};
|
|
220
|
+
|
|
221
|
+
const rules = Object.entries(expectedRules)
|
|
222
|
+
.map(([ruleNumber, ruleName]) => {
|
|
223
|
+
const normative = deriveNormative(ruleName);
|
|
224
|
+
return {
|
|
225
|
+
rule_id: `${categoryKey}-${ruleNumber}`,
|
|
226
|
+
rule_number: ruleNumber,
|
|
227
|
+
rule_name: ruleName,
|
|
228
|
+
reference_url: guidelineUrl,
|
|
229
|
+
severity: priorityToSeverity(priority),
|
|
230
|
+
normative,
|
|
231
|
+
adoption_condition: null,
|
|
232
|
+
evidence_hints: buildEvidenceHints(category)
|
|
233
|
+
};
|
|
234
|
+
})
|
|
235
|
+
.sort((a, b) => compareRuleNumbers(a.rule_number, b.rule_number));
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
feature_id: featureId,
|
|
239
|
+
feature_name: featureName,
|
|
240
|
+
feature_description: category?.description || null,
|
|
241
|
+
guideline_url: guidelineUrl,
|
|
242
|
+
priority,
|
|
243
|
+
|
|
244
|
+
// Helps execution tool routing (PAGINATION, SECURITY, ...)
|
|
245
|
+
category_key: categoryKey,
|
|
246
|
+
|
|
247
|
+
rules
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildEvidenceHints(category) {
|
|
252
|
+
const hints = [];
|
|
253
|
+
if (Array.isArray(category?.applies_to)) hints.push(...category.applies_to);
|
|
254
|
+
if (Array.isArray(category?.keywords)) hints.push(...category.keywords);
|
|
255
|
+
return Array.from(new Set(hints.filter(Boolean).map((h) => h.toString())));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function priorityToSeverity(priority) {
|
|
259
|
+
const p = (priority || "medium").toString().toLowerCase();
|
|
260
|
+
if (p === "critical") return "critical";
|
|
261
|
+
if (p === "high") return "high";
|
|
262
|
+
if (p === "low") return "low";
|
|
263
|
+
return "medium";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function deriveNormative(text) {
|
|
267
|
+
const value = (text || "").toString();
|
|
268
|
+
if (/\bMUST NOT\b/i.test(value)) return "MUST NOT";
|
|
269
|
+
if (/\bSHOULD NOT\b/i.test(value)) return "SHOULD NOT";
|
|
270
|
+
if (/\bMUST\b/i.test(value)) return "MUST";
|
|
271
|
+
if (/\bSHOULD\b/i.test(value)) return "SHOULD";
|
|
272
|
+
if (/\bMAY\b/i.test(value)) return "MAY";
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function compareRuleNumbers(a, b) {
|
|
277
|
+
const aParts = (a || "").toString().split(".").map((x) => Number(x));
|
|
278
|
+
const bParts = (b || "").toString().split(".").map((x) => Number(x));
|
|
279
|
+
|
|
280
|
+
const max = Math.max(aParts.length, bParts.length);
|
|
281
|
+
for (let i = 0; i < max; i++) {
|
|
282
|
+
const av = Number.isFinite(aParts[i]) ? aParts[i] : 0;
|
|
283
|
+
const bv = Number.isFinite(bParts[i]) ? bParts[i] : 0;
|
|
284
|
+
if (av !== bv) return av - bv;
|
|
285
|
+
}
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function computeSummary(features) {
|
|
290
|
+
const summary = {
|
|
291
|
+
total_features: features.length,
|
|
292
|
+
total_rules: 0,
|
|
293
|
+
by_severity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
294
|
+
by_normative: { MUST: 0, SHOULD: 0, MAY: 0, "MUST NOT": 0, "SHOULD NOT": 0, UNKNOWN: 0 }
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
for (const feature of features) {
|
|
298
|
+
for (const rule of feature.rules) {
|
|
299
|
+
summary.total_rules++;
|
|
300
|
+
const sev = rule.severity || "medium";
|
|
301
|
+
if (summary.by_severity[sev] === undefined) summary.by_severity[sev] = 0;
|
|
302
|
+
summary.by_severity[sev]++;
|
|
303
|
+
|
|
304
|
+
const norm = rule.normative || "UNKNOWN";
|
|
305
|
+
if (summary.by_normative[norm] === undefined) summary.by_normative[norm] = 0;
|
|
306
|
+
summary.by_normative[norm]++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return summary;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function formatAsMarkdown(checklist) {
|
|
314
|
+
const lines = [];
|
|
315
|
+
lines.push(`# X-API Readiness Checklist (${checklist.scope})`);
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push(`Generated: ${checklist.generated_at}`);
|
|
318
|
+
if (checklist.template_version) lines.push(`Template: ${checklist.template_version}`);
|
|
319
|
+
lines.push("");
|
|
320
|
+
|
|
321
|
+
for (const feature of checklist.features) {
|
|
322
|
+
lines.push(`## ${feature.feature_name}`);
|
|
323
|
+
if (feature.guideline_url) lines.push(`Source: ${feature.guideline_url}`);
|
|
324
|
+
lines.push("");
|
|
325
|
+
|
|
326
|
+
for (const rule of feature.rules) {
|
|
327
|
+
const norm = rule.normative ? ` (${rule.normative})` : "";
|
|
328
|
+
lines.push(`- [ ] ${rule.rule_id}${norm}: ${rule.rule_name}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
lines.push("");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Backward compatibility for local test runner
|
|
338
|
+
export function formatChecklistAsTable(checklist) {
|
|
339
|
+
return formatAsMarkdown(checklist);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Default export (used by `mcp/test-planner-v2.js`)
|
|
343
|
+
export default generateChecklist;
|
|
Binary file
|