openuispec 0.1.42 → 0.1.43
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/mcp-server/index.ts +143 -4
- package/package.json +1 -1
package/mcp-server/index.ts
CHANGED
|
@@ -22,8 +22,9 @@ import { buildCheckResult } from "../check/index.js";
|
|
|
22
22
|
import { buildStatusResult } from "../status/index.js";
|
|
23
23
|
import { buildValidateResult } from "../schema/validate.js";
|
|
24
24
|
import { loadTargetDrift } from "../drift/index.js";
|
|
25
|
-
import { readFileSync as fsReadFileSync } from "node:fs";
|
|
26
|
-
import { relative } from "node:path";
|
|
25
|
+
import { readFileSync as fsReadFileSync, existsSync, readdirSync } from "node:fs";
|
|
26
|
+
import { relative, resolve } from "node:path";
|
|
27
|
+
import YAML from "yaml";
|
|
27
28
|
|
|
28
29
|
// ── resolve project cwd ──────────────────────────────────────────────
|
|
29
30
|
|
|
@@ -114,15 +115,153 @@ server.registerTool(
|
|
|
114
115
|
|
|
115
116
|
// ── tool: openuispec_check ───────────────────────────────────────────
|
|
116
117
|
|
|
118
|
+
function buildAuditChecklist(projectDir: string, target: string): string {
|
|
119
|
+
const lines: string[] = [
|
|
120
|
+
"POST-GENERATION AUDIT — verify your code against these concrete spec requirements:",
|
|
121
|
+
"",
|
|
122
|
+
"HOW TO AUDIT: For each item below, READ the generated component/screen file,",
|
|
123
|
+
"find the code that implements it, and confirm the values match exactly.",
|
|
124
|
+
"If you cannot find the implementation, it is a REAL GAP — fix it.",
|
|
125
|
+
"",
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Extract must_handle from contracts
|
|
129
|
+
const manifest = YAML.parse(fsReadFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
130
|
+
const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? "./contracts/");
|
|
131
|
+
|
|
132
|
+
if (existsSync(contractsDir)) {
|
|
133
|
+
lines.push("## Contract must_handle requirements");
|
|
134
|
+
for (const file of readdirSync(contractsDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
135
|
+
try {
|
|
136
|
+
const content = YAML.parse(fsReadFileSync(join(contractsDir, file), "utf-8"));
|
|
137
|
+
const contractName = Object.keys(content)[0];
|
|
138
|
+
const contract = content[contractName];
|
|
139
|
+
if (!contract?.variants) continue;
|
|
140
|
+
|
|
141
|
+
for (const [variantName, variant] of Object.entries(contract.variants as Record<string, any>)) {
|
|
142
|
+
const mustHandle = variant?.generation?.must_handle;
|
|
143
|
+
if (mustHandle?.length) {
|
|
144
|
+
lines.push(`\n### ${contractName}.${variantName}`);
|
|
145
|
+
for (const item of mustHandle) {
|
|
146
|
+
lines.push(`- [ ] ${item}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Top-level generation.must_handle
|
|
152
|
+
const topMustHandle = contract?.generation?.must_handle;
|
|
153
|
+
if (topMustHandle?.length) {
|
|
154
|
+
lines.push(`\n### ${contractName} (global)`);
|
|
155
|
+
for (const item of topMustHandle) {
|
|
156
|
+
lines.push(`- [ ] ${item}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch { /* skip unparseable files */ }
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract screens and their sections
|
|
165
|
+
const screensDir = resolve(projectDir, manifest.includes?.screens ?? "./screens/");
|
|
166
|
+
if (existsSync(screensDir)) {
|
|
167
|
+
lines.push("## Screens — verify all sections exist in generated code");
|
|
168
|
+
for (const file of readdirSync(screensDir).filter(f => f.endsWith(".yaml")).sort()) {
|
|
169
|
+
try {
|
|
170
|
+
const content = YAML.parse(fsReadFileSync(join(screensDir, file), "utf-8"));
|
|
171
|
+
const screenName = Object.keys(content)[0];
|
|
172
|
+
const screen = content[screenName];
|
|
173
|
+
if (screen?.status === "stub") continue;
|
|
174
|
+
|
|
175
|
+
const sections: string[] = [];
|
|
176
|
+
const collectSections = (node: any, prefix = "") => {
|
|
177
|
+
if (!node || typeof node !== "object") return;
|
|
178
|
+
if (node.contract) sections.push(`${prefix}${node.contract}${node.variant ? `.${node.variant}` : ""}`);
|
|
179
|
+
if (node.sections) {
|
|
180
|
+
for (const [key, child] of Object.entries(node.sections)) {
|
|
181
|
+
collectSections(child, `${prefix}${key}/`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (node.children && Array.isArray(node.children)) {
|
|
185
|
+
for (const child of node.children) {
|
|
186
|
+
collectSections(child, prefix);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
collectSections(screen?.layout);
|
|
191
|
+
|
|
192
|
+
if (sections.length > 0) {
|
|
193
|
+
lines.push(`\n### ${screenName} (${file})`);
|
|
194
|
+
for (const section of sections) {
|
|
195
|
+
lines.push(`- [ ] ${section}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Adaptive layout
|
|
200
|
+
if (screen?.layout?.adaptive || screen?.adaptive) {
|
|
201
|
+
lines.push(`- [ ] Adaptive breakpoints implemented`);
|
|
202
|
+
}
|
|
203
|
+
} catch { /* skip */ }
|
|
204
|
+
}
|
|
205
|
+
lines.push("");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Locale keys count
|
|
209
|
+
const localesDir = resolve(projectDir, manifest.includes?.locales ?? "./locales/");
|
|
210
|
+
if (existsSync(localesDir)) {
|
|
211
|
+
const localeFiles = readdirSync(localesDir).filter(f => f.endsWith(".json"));
|
|
212
|
+
if (localeFiles.length > 0) {
|
|
213
|
+
lines.push("## Locales — verify all locale files are wired");
|
|
214
|
+
for (const file of localeFiles) {
|
|
215
|
+
try {
|
|
216
|
+
const keys = Object.keys(JSON.parse(fsReadFileSync(join(localesDir, file), "utf-8")));
|
|
217
|
+
lines.push(`- [ ] ${file}: ${keys.length} keys loaded at runtime`);
|
|
218
|
+
} catch { /* skip */ }
|
|
219
|
+
}
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Platform-specific checks
|
|
225
|
+
const platformDir = resolve(projectDir, manifest.includes?.platform ?? "./platform/");
|
|
226
|
+
const platformPath = join(platformDir, `${target}.yaml`);
|
|
227
|
+
if (existsSync(platformPath)) {
|
|
228
|
+
try {
|
|
229
|
+
const platformDoc = YAML.parse(fsReadFileSync(platformPath, "utf-8"));
|
|
230
|
+
const platformDef = platformDoc?.[target];
|
|
231
|
+
if (platformDef?.generation) {
|
|
232
|
+
lines.push("## Platform generation requirements");
|
|
233
|
+
const gen = platformDef.generation;
|
|
234
|
+
if (gen.architecture) lines.push(`- [ ] Architecture: ${gen.architecture}`);
|
|
235
|
+
if (gen.naming) lines.push(`- [ ] Naming convention: ${gen.naming}`);
|
|
236
|
+
if (gen.css) lines.push(`- [ ] CSS framework: ${gen.css}`);
|
|
237
|
+
}
|
|
238
|
+
} catch { /* skip */ }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
lines.push("FOR EACH UNCHECKED ITEM: Read the generated file, search for the implementation,");
|
|
242
|
+
lines.push("and either confirm it matches or fix it. Do not mark items as 'intentionally skipped'");
|
|
243
|
+
lines.push("unless the user explicitly requested to skip them.");
|
|
244
|
+
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
117
248
|
server.registerTool(
|
|
118
249
|
"openuispec_check",
|
|
119
250
|
{
|
|
120
|
-
description: "Run composite validation
|
|
251
|
+
description: "Run composite validation + post-generation audit. Returns schema validation results AND a concrete audit checklist derived from your spec files — listing every contract must_handle item, every screen section, and every locale file that must exist in your generated code. Verify each item.",
|
|
121
252
|
inputSchema: { target: targetSchema },
|
|
122
253
|
},
|
|
123
254
|
async ({ target }) => {
|
|
124
255
|
try {
|
|
125
|
-
|
|
256
|
+
const result = buildCheckResult(target, projectCwd);
|
|
257
|
+
const projectDir = findProjectDir(projectCwd);
|
|
258
|
+
const audit = buildAuditChecklist(projectDir, target);
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{ type: "text" as const, text: JSON.stringify(result, null, 2) },
|
|
262
|
+
{ type: "text" as const, text: audit },
|
|
263
|
+
],
|
|
264
|
+
};
|
|
126
265
|
} catch (err) {
|
|
127
266
|
return toolError(err);
|
|
128
267
|
}
|