openuispec 0.2.16 → 0.2.17

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/check/audit.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * openuispec check --target web --audit --format json
11
11
  */
12
12
 
13
- import { readFileSync } from "node:fs";
13
+ import { existsSync, readFileSync } from "node:fs";
14
14
  import { join, resolve } from "node:path";
15
15
  import YAML from "yaml";
16
16
 
@@ -31,6 +31,16 @@ export interface AuditResult {
31
31
  }
32
32
 
33
33
  const AI_DEFAULT_FONTS = new Set(["Inter", "Roboto", "Arial", "Open Sans"]);
34
+ const REQUIRED_TOKEN_FILES = [
35
+ "color.yaml",
36
+ "typography.yaml",
37
+ "spacing.yaml",
38
+ "elevation.yaml",
39
+ "motion.yaml",
40
+ "layout.yaml",
41
+ "themes.yaml",
42
+ "icons.yaml",
43
+ ];
34
44
 
35
45
  function readYaml(path: string): any {
36
46
  try {
@@ -40,8 +50,36 @@ function readYaml(path: string): any {
40
50
  }
41
51
  }
42
52
 
53
+ function readYamlForAudit(path: string, domain: string, findings: AuditFinding[]): any {
54
+ try {
55
+ return YAML.parse(readFileSync(path, "utf-8"));
56
+ } catch (err: any) {
57
+ if (err?.code === "ENOENT") return null;
58
+ findings.push({
59
+ domain,
60
+ rule: "unreadable_file",
61
+ severity: "error",
62
+ message: `Could not parse "${path}". Fix malformed YAML before relying on this audit.`,
63
+ });
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function checkRequiredTokenFiles(tokensDir: string, findings: AuditFinding[]): void {
69
+ for (const filename of REQUIRED_TOKEN_FILES) {
70
+ if (!existsSync(join(tokensDir, filename))) {
71
+ findings.push({
72
+ domain: "tokens",
73
+ rule: "missing_file",
74
+ severity: "error",
75
+ message: `Required token file "${filename}" is missing.`,
76
+ });
77
+ }
78
+ }
79
+ }
80
+
43
81
  function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
44
- const doc = readYaml(join(tokensDir, "typography.yaml"));
82
+ const doc = readYamlForAudit(join(tokensDir, "typography.yaml"), "typography", findings);
45
83
  if (!doc?.typography) return;
46
84
 
47
85
  // Font diversity: primary must NOT be a common AI default
@@ -68,7 +106,7 @@ function checkTypography(tokensDir: string, findings: AuditFinding[]): void {
68
106
  }
69
107
 
70
108
  function checkColor(tokensDir: string, findings: AuditFinding[]): void {
71
- const doc = readYaml(join(tokensDir, "color.yaml"));
109
+ const doc = readYamlForAudit(join(tokensDir, "color.yaml"), "color", findings);
72
110
  if (!doc?.color) return;
73
111
 
74
112
  // Pure black/white check
@@ -103,8 +141,9 @@ function checkColor(tokensDir: string, findings: AuditFinding[]): void {
103
141
 
104
142
  // Theme coverage: check themes.yaml for both light + dark
105
143
  {
106
- const themes = readYaml(join(tokensDir, "themes.yaml"));
107
- const themeKeys = Object.keys(themes?.themes ?? {});
144
+ const themes = readYamlForAudit(join(tokensDir, "themes.yaml"), "color", findings);
145
+ if (!themes?.themes) return;
146
+ const themeKeys = Object.keys(themes.themes);
108
147
  const hasLight = themeKeys.some((k) => k.includes("light"));
109
148
  const hasDark = themeKeys.some((k) => k.includes("dark"));
110
149
  if (!hasLight || !hasDark) {
@@ -119,7 +158,7 @@ function checkColor(tokensDir: string, findings: AuditFinding[]): void {
119
158
  }
120
159
 
121
160
  function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
122
- const doc = readYaml(join(tokensDir, "spacing.yaml"));
161
+ const doc = readYamlForAudit(join(tokensDir, "spacing.yaml"), "spacing", findings);
123
162
  if (!doc?.spacing) return;
124
163
 
125
164
  // Scale usage: at least 4 distinct values
@@ -156,7 +195,7 @@ function checkSpacing(tokensDir: string, findings: AuditFinding[]): void {
156
195
  }
157
196
 
158
197
  function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
159
- const doc = readYaml(join(tokensDir, "motion.yaml"));
198
+ const doc = readYamlForAudit(join(tokensDir, "motion.yaml"), "motion", findings);
160
199
  if (!doc?.motion) return;
161
200
 
162
201
  // Duration variety: at least 2 distinct durations
@@ -185,7 +224,7 @@ function checkMotion(tokensDir: string, findings: AuditFinding[]): void {
185
224
  function checkContracts(contractsDir: string, findings: AuditFinding[]): void {
186
225
  // All collections have empty_state in must_handle or variants
187
226
  {
188
- const doc = readYaml(join(contractsDir, "collection.yaml"));
227
+ const doc = readYamlForAudit(join(contractsDir, "collection.yaml"), "contracts", findings);
189
228
  const collection = doc ? doc[Object.keys(doc)[0]] : null;
190
229
  if (collection) {
191
230
  const mustHandle: string[] = collection.generation?.must_handle ?? [];
@@ -211,6 +250,7 @@ export function buildAuditResult(projectDir: string, threshold: number = 0): Aud
211
250
  const effectiveThreshold = threshold > 0 ? threshold : (manifest?.generation_guidance?.audit_threshold ?? 0);
212
251
 
213
252
  const findings: AuditFinding[] = [];
253
+ checkRequiredTokenFiles(tokensDir, findings);
214
254
  checkTypography(tokensDir, findings);
215
255
  checkColor(tokensDir, findings);
216
256
  checkSpacing(tokensDir, findings);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openuispec",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A semantic UI specification format for AI-native, platform-native app development",
package/prepare/index.ts CHANGED
@@ -666,8 +666,8 @@ function generationRules(target: string, outputDir: string, manifest: Record<str
666
666
  }
667
667
 
668
668
  function matchesTargetPlatform(item: string, target: string): boolean {
669
- const tagMatch = item.match(/^\[([a-z]+)\]/);
670
- return !tagMatch || tagMatch[1] === target;
669
+ const tagMatch = item.match(/^\[([a-z]+)\]/i);
670
+ return !tagMatch || tagMatch[1].toLowerCase() === target;
671
671
  }
672
672
 
673
673
  function complexityRule(complexity: string): string {
@@ -710,10 +710,10 @@ function buildAntiPatterns(
710
710
 
711
711
  // Contract-specific must_avoid
712
712
  const contract_specific: Record<string, string[]> = {};
713
- try {
714
- const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? './contracts/');
715
- if (existsSync(contractsDir)) {
716
- for (const file of readdirSync(contractsDir).filter((f) => f.endsWith('.yaml') && !f.startsWith('x_'))) {
713
+ const contractsDir = resolve(projectDir, manifest.includes?.contracts ?? './contracts/');
714
+ if (existsSync(contractsDir)) {
715
+ for (const file of readdirSync(contractsDir).filter((f) => f.endsWith('.yaml') && !f.startsWith('x_'))) {
716
+ try {
717
717
  const content = YAML.parse(readFileSync(join(contractsDir, file), 'utf-8'));
718
718
  const contractName = Object.keys(content)[0];
719
719
  const mustAvoid: string[] = content[contractName]?.generation?.must_avoid ?? [];
@@ -721,9 +721,11 @@ function buildAntiPatterns(
721
721
  const filtered = mustAvoid.filter((item: string) => matchesTargetPlatform(item, target));
722
722
  if (filtered.length > 0) contract_specific[contractName] = filtered;
723
723
  }
724
+ } catch {
725
+ continue;
724
726
  }
725
727
  }
726
- } catch { /* skip on error */ }
728
+ }
727
729
 
728
730
  // Project-specific avoid from design section
729
731
  const project_specific: string[] = [];
@@ -1386,6 +1388,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
1386
1388
  const sharedLayerConfigs = sharedLayersForTarget(projectDir, target);
1387
1389
  const codeRoots = suggestCodeRoots(target, outputDir, projectDir, sharedLayerConfigs);
1388
1390
  const manifest = readManifest(projectDir);
1391
+ const antiPatterns = buildAntiPatterns(manifest, projectDir, target);
1392
+ const designContext = buildDesignContext(manifest);
1389
1393
  const sharedLayerInfos = buildSharedLayerInfos(projectDir, target, sharedLayerConfigs);
1390
1394
  const platformDef = readPlatformDefinition(projectDir, manifest, target);
1391
1395
  const platformConfig = buildPlatformConfig(target, platformDef);
@@ -1429,6 +1433,8 @@ function buildUpdatePrepareResult(cwd: string, target: string, includeContents:
1429
1433
  items,
1430
1434
  ...(sharedLayerInfos.length > 0 ? { shared_layers: sharedLayerInfos } : {}),
1431
1435
  ...(includeContents ? { spec_contents: readAllSpecContents(projectDir) } : {}),
1436
+ ...(antiPatterns ? { anti_patterns: antiPatterns } : {}),
1437
+ ...(designContext ? { design_context: designContext } : {}),
1432
1438
  next_steps: nextSteps,
1433
1439
  };
1434
1440
  }
@@ -399,6 +399,16 @@ function buildAjv(): AjvInstance {
399
399
  }
400
400
 
401
401
  const BASE = "https://openuispec.rsteam.uz/schema/";
402
+ const TOKEN_FILE_SCHEMAS: Record<string, string> = {
403
+ "color.yaml": "color.schema.json",
404
+ "typography.yaml": "typography.schema.json",
405
+ "spacing.yaml": "spacing.schema.json",
406
+ "elevation.yaml": "elevation.schema.json",
407
+ "motion.yaml": "motion.schema.json",
408
+ "layout.yaml": "layout.schema.json",
409
+ "themes.yaml": "themes.schema.json",
410
+ "icons.yaml": "icons.schema.json",
411
+ };
402
412
 
403
413
  // ── validate one file ────────────────────────────────────────────────
404
414
 
@@ -536,20 +546,13 @@ const GROUPS: Record<string, ValidationGroup> = {
536
546
  run(ajv, projectDir, includes) {
537
547
  let errors = 0;
538
548
  const tokensDir = resolveInclude(projectDir, includes.tokens);
539
- const tokenMap: Record<string, string> = {
540
- "color.yaml": "color.schema.json",
541
- "typography.yaml": "typography.schema.json",
542
- "spacing.yaml": "spacing.schema.json",
543
- "elevation.yaml": "elevation.schema.json",
544
- "motion.yaml": "motion.schema.json",
545
- "layout.yaml": "layout.schema.json",
546
- "themes.yaml": "themes.schema.json",
547
- "icons.yaml": "icons.schema.json",
548
- };
549
- for (const [data, schema] of Object.entries(tokenMap)) {
549
+ for (const [data, schema] of Object.entries(TOKEN_FILE_SCHEMAS)) {
550
550
  const filePath = join(tokensDir, data);
551
551
  if (existsSync(filePath)) {
552
552
  errors += validateFile(ajv, filePath, `${BASE}tokens/${schema}`);
553
+ } else {
554
+ console.log(` FAIL ${data} (required token file is missing)`);
555
+ errors += 1;
553
556
  }
554
557
  }
555
558
  return errors;
@@ -557,20 +560,16 @@ const GROUPS: Record<string, ValidationGroup> = {
557
560
  collectJson(ajv, projectDir, includes, groupKey) {
558
561
  const errors: JsonError[] = [];
559
562
  const tokensDir = resolveInclude(projectDir, includes.tokens);
560
- const tokenMap: Record<string, string> = {
561
- "color.yaml": "color.schema.json",
562
- "typography.yaml": "typography.schema.json",
563
- "spacing.yaml": "spacing.schema.json",
564
- "elevation.yaml": "elevation.schema.json",
565
- "motion.yaml": "motion.schema.json",
566
- "layout.yaml": "layout.schema.json",
567
- "themes.yaml": "themes.schema.json",
568
- "icons.yaml": "icons.schema.json",
569
- };
570
- for (const [data, schema] of Object.entries(tokenMap)) {
563
+ for (const [data, schema] of Object.entries(TOKEN_FILE_SCHEMAS)) {
571
564
  const filePath = join(tokensDir, data);
572
565
  if (existsSync(filePath)) {
573
566
  errors.push(...collectValidateFile(ajv, filePath, `${BASE}tokens/${schema}`));
567
+ } else {
568
+ errors.push({
569
+ file: data,
570
+ path: "(root)",
571
+ message: "required token file is missing",
572
+ });
574
573
  }
575
574
  }
576
575
  return { group: groupKey, errors };