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.
Files changed (2) hide show
  1. package/mcp-server/index.ts +143 -4
  2. package/package.json +1 -1
@@ -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: schema validation, semantic linting, and prepare readiness check. Call after spec edits to verify correctness.",
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
- return toolResult(buildCheckResult(target, projectCwd));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",