x-readiness-mcp 0.3.0 → 0.5.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.
- package/README.md +325 -0
- package/USAGE_GUIDE.md +271 -0
- package/package.json +11 -3
- 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
- package/x-readiness-mcp-0.3.0.tgz +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// mcp/tools/gitignore-helper.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensures .xreadiness/ is ignored by git
|
|
7
|
+
* Tries .gitignore first, falls back to .git/info/exclude
|
|
8
|
+
*/
|
|
9
|
+
export async function ensureXReadinessIgnored(repoPath) {
|
|
10
|
+
const gitignorePath = path.join(repoPath, ".gitignore");
|
|
11
|
+
const gitExcludePath = path.join(repoPath, ".git", "info", "exclude");
|
|
12
|
+
const ignoreEntry = ".xreadiness/";
|
|
13
|
+
|
|
14
|
+
// Try to add to .gitignore
|
|
15
|
+
try {
|
|
16
|
+
let gitignoreContent = "";
|
|
17
|
+
try {
|
|
18
|
+
gitignoreContent = await fs.readFile(gitignorePath, "utf8");
|
|
19
|
+
} catch {
|
|
20
|
+
// File doesn't exist, will create it
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if already present
|
|
24
|
+
if (gitignoreContent.includes(ignoreEntry)) {
|
|
25
|
+
return {
|
|
26
|
+
status: "ALREADY_IGNORED",
|
|
27
|
+
method: ".gitignore",
|
|
28
|
+
message: ".xreadiness/ is already in .gitignore"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Append to .gitignore
|
|
33
|
+
const newContent = gitignoreContent.trim()
|
|
34
|
+
? `${gitignoreContent.trim()}\n\n# X-Readiness tool artifacts\n${ignoreEntry}\n`
|
|
35
|
+
: `# X-Readiness tool artifacts\n${ignoreEntry}\n`;
|
|
36
|
+
|
|
37
|
+
await fs.writeFile(gitignorePath, newContent, "utf8");
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
status: "ADDED_TO_GITIGNORE",
|
|
41
|
+
method: ".gitignore",
|
|
42
|
+
path: gitignorePath,
|
|
43
|
+
message: `✅ Added ${ignoreEntry} to .gitignore`
|
|
44
|
+
};
|
|
45
|
+
} catch (gitignoreErr) {
|
|
46
|
+
// Fallback to .git/info/exclude
|
|
47
|
+
try {
|
|
48
|
+
let excludeContent = "";
|
|
49
|
+
try {
|
|
50
|
+
excludeContent = await fs.readFile(gitExcludePath, "utf8");
|
|
51
|
+
} catch {
|
|
52
|
+
// Create directory if needed
|
|
53
|
+
await fs.mkdir(path.dirname(gitExcludePath), { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if already present
|
|
57
|
+
if (excludeContent.includes(ignoreEntry)) {
|
|
58
|
+
return {
|
|
59
|
+
status: "ALREADY_IGNORED",
|
|
60
|
+
method: ".git/info/exclude",
|
|
61
|
+
message: ".xreadiness/ is already in .git/info/exclude"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Append to exclude
|
|
66
|
+
const newContent = excludeContent.trim()
|
|
67
|
+
? `${excludeContent.trim()}\n\n# X-Readiness tool artifacts\n${ignoreEntry}\n`
|
|
68
|
+
: `# X-Readiness tool artifacts\n${ignoreEntry}\n`;
|
|
69
|
+
|
|
70
|
+
await fs.writeFile(gitExcludePath, newContent, "utf8");
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
status: "ADDED_TO_EXCLUDE",
|
|
74
|
+
method: ".git/info/exclude",
|
|
75
|
+
path: gitExcludePath,
|
|
76
|
+
message: `✅ Added ${ignoreEntry} to .git/info/exclude (local ignore)`
|
|
77
|
+
};
|
|
78
|
+
} catch (excludeErr) {
|
|
79
|
+
return {
|
|
80
|
+
status: "FAILED",
|
|
81
|
+
error: `Could not add to .gitignore or .git/info/exclude`,
|
|
82
|
+
gitignore_error: gitignoreErr.message,
|
|
83
|
+
exclude_error: excludeErr.message,
|
|
84
|
+
message: "⚠️ Failed to auto-ignore .xreadiness/. Please add it manually to .gitignore"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
package/tools/planning.js
CHANGED
|
@@ -1,343 +1,130 @@
|
|
|
1
|
-
// tools/planning.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
});
|
|
1
|
+
// mcp/tools/planning.js
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { generateChecklist } from "./template-parser.js";
|
|
6
|
+
import { ensureXReadinessIgnored } from "./gitignore-helper.js";
|
|
7
|
+
|
|
8
|
+
const PLAN_DIR_NAME = [".xreadiness", "plan"];
|
|
9
|
+
|
|
10
|
+
function planDir(repoPath) {
|
|
11
|
+
return path.resolve(repoPath, ...PLAN_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function ensureDir(p) {
|
|
15
|
+
await fs.mkdir(p, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate plan markdown with high-level checklist
|
|
20
|
+
*/
|
|
21
|
+
function toPlanMarkdown({ planId, repoPath, scope, developerPrompt, checklist }) {
|
|
22
|
+
const totalRules = checklist.total_rules;
|
|
23
|
+
|
|
24
|
+
let md = `# X-Readiness Plan\n\n`;
|
|
25
|
+
md += `**Generated:** ${new Date().toISOString()}\n\n`;
|
|
26
|
+
md += `- **Plan ID:** ${planId}\n`;
|
|
27
|
+
md += `- **Repository Path:** ${repoPath}\n`;
|
|
28
|
+
md += `- **Scope:** ${scope}\n`;
|
|
29
|
+
md += `- **Scope Description:** ${checklist.scope_description}\n`;
|
|
30
|
+
md += `- **Developer Prompt:** ${developerPrompt}\n`;
|
|
31
|
+
md += `- **Total Rules Selected:** ${totalRules}\n`;
|
|
32
|
+
md += `- **Template Version:** ${checklist.template_version}\n\n`;
|
|
33
|
+
|
|
34
|
+
md += `---\n\n`;
|
|
35
|
+
md += `## Reference URLs\n\n`;
|
|
36
|
+
md += `- Common Guidelines: ${checklist.reference_urls.common}\n`;
|
|
37
|
+
md += `- REST Guidelines: ${checklist.reference_urls.rest}\n\n`;
|
|
38
|
+
|
|
39
|
+
md += `---\n\n`;
|
|
40
|
+
md += `## High-Level Checklist\n\n`;
|
|
41
|
+
md += `This plan will check the following categories and rules:\n\n`;
|
|
42
|
+
|
|
43
|
+
for (const category of checklist.categories) {
|
|
44
|
+
md += `### ${category.name} (Priority: ${category.priority})\n\n`;
|
|
45
|
+
md += `**Guideline:** [${category.name}](${category.guideline_url})\n\n`;
|
|
46
|
+
md += `**Rules to check:**\n\n`;
|
|
47
|
+
|
|
48
|
+
for (const rule of category.rules) {
|
|
49
|
+
md += `- [ ] **${rule.rule_id}** — ${rule.rule_name}\n`;
|
|
181
50
|
}
|
|
51
|
+
md += `\n`;
|
|
182
52
|
}
|
|
183
53
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
54
|
+
md += `---\n\n`;
|
|
55
|
+
md += `## Next Steps\n\n`;
|
|
56
|
+
md += `1. **Review this plan** to ensure it covers the requirements you need\n`;
|
|
57
|
+
md += `2. **⚠️ IMPORTANT:** Do NOT proceed with execution automatically\n`;
|
|
58
|
+
md += `3. **When ready to execute**, explicitly reply: **"Yes, run execution"**\n`;
|
|
59
|
+
md += `4. **Execution will require your approval code** before scanning your source code\n`;
|
|
60
|
+
md += `5. **To execute**, call the \`execute\` tool with:\n`;
|
|
61
|
+
md += ` - \`repoPath\`: ${repoPath}\n`;
|
|
62
|
+
md += ` - \`planPath\`: (path to this plan's JSON file)\n\n`;
|
|
63
|
+
|
|
64
|
+
return md;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Main planning tool - generates plan and asks for approval to execute
|
|
69
|
+
*/
|
|
70
|
+
export async function planTool({ repoPath, scope, developerPrompt, outputFormat = "markdown" }) {
|
|
71
|
+
// Ensure .xreadiness/ is in .gitignore
|
|
72
|
+
const gitignoreResult = await ensureXReadinessIgnored(repoPath);
|
|
73
|
+
|
|
74
|
+
// Generate checklist from template based on scope and developer prompt
|
|
75
|
+
const checklist = await generateChecklist(scope, developerPrompt);
|
|
76
|
+
|
|
77
|
+
// Create unique plan ID
|
|
78
|
+
const planId = `plan_${new Date().toISOString().replace(/[:.]/g, "-")}_${crypto.randomUUID().slice(0, 8)}`;
|
|
79
|
+
const dir = planDir(repoPath);
|
|
80
|
+
await ensureDir(dir);
|
|
81
|
+
|
|
82
|
+
const planJsonPath = path.join(dir, `${planId}.json`);
|
|
83
|
+
const planMdPath = path.join(dir, `${planId}.md`);
|
|
84
|
+
|
|
85
|
+
// Build plan object
|
|
86
|
+
const plan = {
|
|
87
|
+
plan_id: planId,
|
|
88
|
+
scope,
|
|
89
|
+
scope_description: checklist.scope_description,
|
|
90
|
+
developer_prompt: developerPrompt,
|
|
91
|
+
created_at: new Date().toISOString(),
|
|
92
|
+
template_version: checklist.template_version,
|
|
93
|
+
reference_urls: checklist.reference_urls,
|
|
94
|
+
categories: checklist.categories,
|
|
95
|
+
total_rules: checklist.total_rules
|
|
208
96
|
};
|
|
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
97
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
}
|
|
98
|
+
// Write plan to JSON
|
|
99
|
+
await fs.writeFile(planJsonPath, JSON.stringify(plan, null, 2), "utf8");
|
|
265
100
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
101
|
+
// Write plan to Markdown
|
|
102
|
+
if (outputFormat === "markdown" || outputFormat === "both") {
|
|
103
|
+
const md = toPlanMarkdown({ planId, repoPath, scope, developerPrompt, checklist });
|
|
104
|
+
await fs.writeFile(planMdPath, md, "utf8");
|
|
285
105
|
}
|
|
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
106
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
107
|
+
return {
|
|
108
|
+
status: "PLAN_GENERATED",
|
|
109
|
+
plan_id: planId,
|
|
110
|
+
plan_json_path: planJsonPath,
|
|
111
|
+
plan_md_path: planMdPath,
|
|
112
|
+
gitignore_status: gitignoreResult,
|
|
113
|
+
summary: {
|
|
114
|
+
scope,
|
|
115
|
+
scope_description: checklist.scope_description,
|
|
116
|
+
total_rules: checklist.total_rules,
|
|
117
|
+
categories_count: checklist.categories.length
|
|
118
|
+
},
|
|
119
|
+
message: `✅ Plan generated successfully!\n\n📄 Plan saved to:\n- JSON: ${planJsonPath}\n- Markdown: ${planMdPath}\n\n${gitignoreResult.message}\n\n📋 This plan includes ${checklist.total_rules} rules across ${checklist.categories.length} categories.\n\n⚠️ NEXT STEPS:\n1. Review the plan carefully\n2. When ready, explicitly reply: "Yes, run execution"\n3. Do NOT proceed with execution automatically\n4. Execution will require an approval code before scanning your source code`,
|
|
120
|
+
instruction: "⚠️ Reply 'Yes, run execution' to proceed. Do NOT run execution without explicit user approval.",
|
|
121
|
+
next_step: {
|
|
122
|
+
tool: "execute",
|
|
123
|
+
parameters: {
|
|
124
|
+
repoPath: repoPath,
|
|
125
|
+
planPath: planJsonPath
|
|
126
|
+
},
|
|
127
|
+
note: "⚠️ Execution requires approval code. Wait for user confirmation before calling this tool."
|
|
329
128
|
}
|
|
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);
|
|
129
|
+
};
|
|
340
130
|
}
|
|
341
|
-
|
|
342
|
-
// Default export (used by `mcp/test-planner-v2.js`)
|
|
343
|
-
export default generateChecklist;
|