openuispec 0.1.41 → 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/cli/init.ts +22 -15
- package/mcp-server/index.ts +161 -10
- package/package.json +1 -1
package/cli/init.ts
CHANGED
|
@@ -319,21 +319,28 @@ or any visual/structural change — you MUST use the OpenUISpec tools before wri
|
|
|
319
319
|
|
|
320
320
|
Call these MCP tools directly. They return structured JSON with everything you need.
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
**Pre-generation:**
|
|
323
|
+
1. Call \`openuispec_prepare\` with the target platform — returns spec context, platform config, constraints.
|
|
324
|
+
2. Call \`openuispec_read_specs\` to load spec file contents. Use these as the AUTHORITATIVE source.
|
|
325
|
+
3. If spec changes are needed, update spec files FIRST, then call \`openuispec_check\`.
|
|
326
|
+
4. Generate or update the platform UI code based on the spec contents.
|
|
327
|
+
|
|
328
|
+
**Post-generation (EVERY TIME after writing UI code):**
|
|
329
|
+
5. Call \`openuispec_check\` to validate spec integrity.
|
|
330
|
+
6. Call \`openuispec_read_specs\` for the screens/contracts you just generated code for.
|
|
331
|
+
7. Audit your generated code against the spec. For each screen, verify:
|
|
332
|
+
- Every field/action in the spec has a corresponding UI element
|
|
333
|
+
- Token values (colors, spacing, radii) match exactly — no approximations
|
|
334
|
+
- Contract \`must_handle\` states are all implemented (loading, error, empty, etc.)
|
|
335
|
+
- Adaptive breakpoints match the spec's \`size_classes\`
|
|
336
|
+
- Locale keys match \`$t:\` references
|
|
337
|
+
- Navigation targets match flow definitions
|
|
338
|
+
8. Report any real gaps found and fix them before finishing.
|
|
339
|
+
|
|
340
|
+
**Other tools:**
|
|
341
|
+
- \`openuispec_status\` — cross-target summary, good starting point
|
|
342
|
+
- \`openuispec_drift\` with \`explain: true\` — property-level spec changes
|
|
343
|
+
- \`openuispec_validate\` — schema-only validation by group
|
|
337
344
|
|
|
338
345
|
### CLI fallback (when MCP is not available)
|
|
339
346
|
|
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
|
|
|
@@ -69,14 +70,26 @@ const server = new McpServer(
|
|
|
69
70
|
Spec files (YAML) are the single source of truth for all UI across platforms.
|
|
70
71
|
|
|
71
72
|
MANDATORY WORKFLOW for any UI-related request (screens, navigation, layout, tokens, flows, localization):
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
PRE-GENERATION:
|
|
75
|
+
1. Call openuispec_prepare with the target platform.
|
|
76
|
+
2. Call openuispec_read_specs to load the spec file contents you need.
|
|
76
77
|
Use the returned contents as the AUTHORITATIVE source — do NOT paraphrase from memory.
|
|
77
78
|
Cross-reference exact token values, contract must_handle lists, and locale keys from the content.
|
|
78
|
-
3. If the request requires spec changes, update the spec files FIRST, then call openuispec_check
|
|
79
|
-
4.
|
|
79
|
+
3. If the request requires spec changes, update the spec files FIRST, then call openuispec_check.
|
|
80
|
+
4. Generate or update the platform UI code based on the spec contents.
|
|
81
|
+
|
|
82
|
+
POST-GENERATION (do this EVERY TIME after writing UI code):
|
|
83
|
+
5. Call openuispec_check to validate spec integrity.
|
|
84
|
+
6. Call openuispec_read_specs for the screens/contracts you just generated code for.
|
|
85
|
+
7. Audit your generated code against the spec contents. For each screen, verify:
|
|
86
|
+
- Every field/action in the spec has a corresponding UI element
|
|
87
|
+
- Token values (colors, spacing, radii) match the spec exactly, not approximations
|
|
88
|
+
- Contract must_handle states are all implemented (loading, error, empty, etc.)
|
|
89
|
+
- Adaptive breakpoints match the layout size_classes in the spec
|
|
90
|
+
- Locale keys match $t: references in the spec
|
|
91
|
+
- Navigation targets match flow definitions
|
|
92
|
+
Report any real gaps found and fix them before finishing.
|
|
80
93
|
|
|
81
94
|
Skip these tools ONLY when the request is purely non-UI (API logic, database, infrastructure, etc.)
|
|
82
95
|
or explicitly platform-specific polish that doesn't affect shared UI semantics.`,
|
|
@@ -102,15 +115,153 @@ server.registerTool(
|
|
|
102
115
|
|
|
103
116
|
// ── tool: openuispec_check ───────────────────────────────────────────
|
|
104
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
|
+
|
|
105
248
|
server.registerTool(
|
|
106
249
|
"openuispec_check",
|
|
107
250
|
{
|
|
108
|
-
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.",
|
|
109
252
|
inputSchema: { target: targetSchema },
|
|
110
253
|
},
|
|
111
254
|
async ({ target }) => {
|
|
112
255
|
try {
|
|
113
|
-
|
|
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
|
+
};
|
|
114
265
|
} catch (err) {
|
|
115
266
|
return toolError(err);
|
|
116
267
|
}
|