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 +48 -8
- package/package.json +1 -1
- package/prepare/index.ts +13 -7
- package/schema/validate.ts +21 -22
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 =
|
|
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 =
|
|
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 =
|
|
107
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
}
|
|
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
|
}
|
package/schema/validate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 };
|