openuispec 0.2.19 → 0.2.20

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 (38) hide show
  1. package/dist/check/audit.js +392 -0
  2. package/dist/check/index.js +216 -0
  3. package/dist/cli/configure-target.js +391 -0
  4. package/dist/cli/index.js +510 -0
  5. package/dist/cli/init.js +1047 -0
  6. package/dist/drift/index.js +903 -0
  7. package/dist/mcp-server/index.js +886 -0
  8. package/dist/mcp-server/preview-render.js +1761 -0
  9. package/dist/mcp-server/preview.js +233 -0
  10. package/dist/mcp-server/screenshot-android.js +458 -0
  11. package/dist/mcp-server/screenshot-ios.js +639 -0
  12. package/dist/mcp-server/screenshot-shared.js +180 -0
  13. package/dist/mcp-server/screenshot.js +459 -0
  14. package/dist/prepare/index.js +1216 -0
  15. package/dist/runtime/package-paths.js +33 -0
  16. package/dist/schema/semantic-lint.js +564 -0
  17. package/dist/schema/validate.js +689 -0
  18. package/dist/status/index.js +194 -0
  19. package/package.json +12 -13
  20. package/check/audit.ts +0 -426
  21. package/check/index.ts +0 -320
  22. package/cli/configure-target.ts +0 -523
  23. package/cli/index.ts +0 -537
  24. package/cli/init.ts +0 -1253
  25. package/drift/index.ts +0 -1165
  26. package/mcp-server/index.ts +0 -1041
  27. package/mcp-server/preview-render.ts +0 -1922
  28. package/mcp-server/preview.ts +0 -292
  29. package/mcp-server/screenshot-android.ts +0 -621
  30. package/mcp-server/screenshot-ios.ts +0 -753
  31. package/mcp-server/screenshot-shared.ts +0 -237
  32. package/mcp-server/screenshot.ts +0 -563
  33. package/prepare/index.ts +0 -1530
  34. package/schema/semantic-lint.ts +0 -692
  35. package/schema/validate.ts +0 -870
  36. package/scripts/regenerate-previews.ts +0 -136
  37. package/scripts/take-all-screenshots.ts +0 -507
  38. package/status/index.ts +0 -275
@@ -0,0 +1,33 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const packageRootCache = new Map();
5
+ function moduleDir(importMetaUrl) {
6
+ return dirname(fileURLToPath(importMetaUrl));
7
+ }
8
+ export function findPackageRoot(importMetaUrl) {
9
+ const startDir = moduleDir(importMetaUrl);
10
+ const cached = packageRootCache.get(startDir);
11
+ if (cached) {
12
+ return cached;
13
+ }
14
+ let current = startDir;
15
+ while (true) {
16
+ if (existsSync(join(current, "package.json"))) {
17
+ packageRootCache.set(startDir, current);
18
+ return current;
19
+ }
20
+ const parent = dirname(current);
21
+ if (parent === current) {
22
+ throw new Error(`Could not locate package.json from ${startDir}`);
23
+ }
24
+ current = parent;
25
+ }
26
+ }
27
+ export function resolvePackagePath(importMetaUrl, ...segments) {
28
+ return join(findPackageRoot(importMetaUrl), ...segments);
29
+ }
30
+ export function readPackageVersion(importMetaUrl) {
31
+ const pkg = JSON.parse(readFileSync(resolvePackagePath(importMetaUrl, "package.json"), "utf-8"));
32
+ return pkg.version ?? "unknown";
33
+ }
@@ -0,0 +1,564 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import YAML from "yaml";
4
+ import { listFiles, readManifest } from "../drift/index.js";
5
+ /** Collect all locale keys from a JSON object, supporting both flat dotted keys and nested objects. */
6
+ function collectLocaleKeys(data, prefix = "") {
7
+ if (!data || typeof data !== "object" || Array.isArray(data))
8
+ return [];
9
+ const keys = [];
10
+ for (const [key, value] of Object.entries(data)) {
11
+ if (key.startsWith("$"))
12
+ continue; // skip $locale, $direction
13
+ const fullKey = prefix ? `${prefix}.${key}` : key;
14
+ if (typeof value === "string") {
15
+ keys.push(fullKey);
16
+ }
17
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
18
+ // Nested object — recurse to flatten
19
+ keys.push(...collectLocaleKeys(value, fullKey));
20
+ }
21
+ }
22
+ return keys;
23
+ }
24
+ const BUILTIN_CONTRACTS = new Set([
25
+ "nav_container",
26
+ "surface",
27
+ "action_trigger",
28
+ "input_field",
29
+ "data_display",
30
+ "collection",
31
+ "feedback",
32
+ ]);
33
+ const BUILTIN_FORMATTERS = new Set([
34
+ "date",
35
+ "time",
36
+ "datetime",
37
+ "date_relative",
38
+ "currency",
39
+ "number",
40
+ "percent",
41
+ "boolean",
42
+ "duration",
43
+ ]);
44
+ function loadJson(filePath) {
45
+ return JSON.parse(readFileSync(filePath, "utf-8"));
46
+ }
47
+ function loadYaml(filePath) {
48
+ return YAML.parse(readFileSync(filePath, "utf-8"));
49
+ }
50
+ function loadData(filePath) {
51
+ return filePath.endsWith(".json") ? loadJson(filePath) : loadYaml(filePath);
52
+ }
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function getSingleRootValue(data) {
57
+ if (!isRecord(data))
58
+ return undefined;
59
+ const values = Object.values(data);
60
+ return values.length === 1 ? values[0] : undefined;
61
+ }
62
+ function rootKeys(filePath) {
63
+ const data = loadData(filePath);
64
+ if (!isRecord(data))
65
+ return [];
66
+ return Object.keys(data);
67
+ }
68
+ function addNestedObjectKeys(prefix, value, refs) {
69
+ if (!isRecord(value))
70
+ return;
71
+ for (const [key, nested] of Object.entries(value)) {
72
+ const next = `${prefix}.${key}`;
73
+ refs.add(next);
74
+ addNestedObjectKeys(next, nested, refs);
75
+ }
76
+ }
77
+ function collectTokenRefs(root, value, refs) {
78
+ if (!root || !isRecord(value))
79
+ return;
80
+ refs.add(root);
81
+ if (root === "spacing") {
82
+ for (const group of ["scale", "aliases"]) {
83
+ const groupValue = value[group];
84
+ if (!isRecord(groupValue))
85
+ continue;
86
+ for (const [key, nested] of Object.entries(groupValue)) {
87
+ const ref = `${root}.${key}`;
88
+ refs.add(ref);
89
+ addNestedObjectKeys(ref, nested, refs);
90
+ }
91
+ }
92
+ return;
93
+ }
94
+ if (root === "typography") {
95
+ for (const group of ["scale", "font_family"]) {
96
+ const groupValue = value[group];
97
+ if (!isRecord(groupValue))
98
+ continue;
99
+ for (const [key, nested] of Object.entries(groupValue)) {
100
+ const ref = group === "scale" ? `${root}.${key}` : `${root}.${group}.${key}`;
101
+ refs.add(ref);
102
+ addNestedObjectKeys(ref, nested, refs);
103
+ }
104
+ }
105
+ return;
106
+ }
107
+ if (root === "elevation") {
108
+ for (const [key, nested] of Object.entries(value)) {
109
+ const ref = `${root}.${key}`;
110
+ refs.add(ref);
111
+ addNestedObjectKeys(ref, nested, refs);
112
+ }
113
+ return;
114
+ }
115
+ if (root === "color") {
116
+ addNestedObjectKeys(root, value, refs);
117
+ }
118
+ }
119
+ function collectApiRefs(prefix, value, refs) {
120
+ if (!isRecord(value))
121
+ return;
122
+ const looksLikeEndpoint = typeof value.method === "string" || typeof value.path === "string";
123
+ if (looksLikeEndpoint && prefix) {
124
+ refs.add(`api.${prefix}`);
125
+ }
126
+ for (const [key, nested] of Object.entries(value)) {
127
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
128
+ collectApiRefs(nextPrefix, nested, refs);
129
+ }
130
+ }
131
+ function iconNameWithVariant(name, variant) {
132
+ if (!variant)
133
+ return name;
134
+ if (variant.startsWith("_"))
135
+ return `${name}${variant}`;
136
+ return `${name}_${variant}`;
137
+ }
138
+ function collectIconRefs(filePath) {
139
+ const refs = new Set();
140
+ const suffixes = [];
141
+ const data = loadData(filePath);
142
+ if (!isRecord(data) || !isRecord(data.icons))
143
+ return { refs, suffixes };
144
+ const icons = data.icons;
145
+ const variants = isRecord(icons.variants) ? icons.variants : {};
146
+ const variantSuffixes = isRecord(variants.suffixes) ? variants.suffixes : {};
147
+ for (const suffix of Object.keys(variantSuffixes)) {
148
+ if (suffix.trim())
149
+ suffixes.push(suffix);
150
+ }
151
+ const registerEntry = (name, value) => {
152
+ refs.add(name);
153
+ if (!isRecord(value))
154
+ return;
155
+ if (Array.isArray(value.variants)) {
156
+ for (const variant of value.variants) {
157
+ if (typeof variant === "string" && variant.trim()) {
158
+ refs.add(iconNameWithVariant(name, variant));
159
+ }
160
+ }
161
+ }
162
+ };
163
+ const registry = isRecord(icons.registry) ? icons.registry : {};
164
+ for (const groupValue of Object.values(registry)) {
165
+ if (!isRecord(groupValue))
166
+ continue;
167
+ for (const [name, value] of Object.entries(groupValue)) {
168
+ registerEntry(name, value);
169
+ }
170
+ }
171
+ const custom = isRecord(icons.custom) ? icons.custom : {};
172
+ for (const namespaceValue of Object.values(custom)) {
173
+ if (!isRecord(namespaceValue))
174
+ continue;
175
+ for (const [name, value] of Object.entries(namespaceValue)) {
176
+ registerEntry(name, value);
177
+ }
178
+ }
179
+ const fallback = isRecord(icons.fallback) ? icons.fallback : {};
180
+ if (typeof fallback.missing_icon === "string") {
181
+ refs.add(fallback.missing_icon);
182
+ }
183
+ return { refs, suffixes };
184
+ }
185
+ function buildContext(projectDir, includes, manifest) {
186
+ const localeDir = resolve(projectDir, includes.locales);
187
+ const localeFiles = new Map();
188
+ for (const filePath of listFiles(localeDir, ".json")) {
189
+ const localeName = basename(filePath, ".json");
190
+ const data = loadJson(filePath);
191
+ // Support both flat dotted keys ("nav.home": "Home") and nested objects ({ nav: { home: "Home" } })
192
+ const flatKeys = Object.keys(data).filter(k => !k.startsWith("$"));
193
+ const hasNestedObjects = flatKeys.some(k => {
194
+ const v = data[k];
195
+ return typeof v === "object" && v !== null && !Array.isArray(v);
196
+ });
197
+ const allKeys = hasNestedObjects ? collectLocaleKeys(data) : flatKeys;
198
+ localeFiles.set(localeName, new Set(allKeys));
199
+ }
200
+ const formatterNames = new Set([
201
+ ...BUILTIN_FORMATTERS,
202
+ ...(isRecord(manifest.formatters) ? Object.keys(manifest.formatters) : []),
203
+ ]);
204
+ const mapperNames = new Set(isRecord(manifest.mappers) ? Object.keys(manifest.mappers) : []);
205
+ const contractNames = new Set(BUILTIN_CONTRACTS);
206
+ const contractsDir = resolve(projectDir, includes.contracts);
207
+ for (const filePath of listFiles(contractsDir, ".yaml")) {
208
+ for (const key of rootKeys(filePath)) {
209
+ contractNames.add(key);
210
+ }
211
+ }
212
+ // Components are also valid references in screen sections (via "component:" key)
213
+ const componentNames = new Set();
214
+ const componentsDir = resolve(projectDir, includes.components);
215
+ for (const filePath of listFiles(componentsDir, ".yaml")) {
216
+ for (const key of rootKeys(filePath)) {
217
+ componentNames.add(key);
218
+ }
219
+ }
220
+ const tokenRefs = new Set();
221
+ const tokensDir = resolve(projectDir, includes.tokens);
222
+ for (const filePath of listFiles(tokensDir, ".yaml")) {
223
+ const data = loadData(filePath);
224
+ if (!isRecord(data))
225
+ continue;
226
+ for (const [root, value] of Object.entries(data)) {
227
+ collectTokenRefs(root, value, tokenRefs);
228
+ }
229
+ }
230
+ const iconsPath = join(tokensDir, "icons.yaml");
231
+ const iconData = existsSync(iconsPath)
232
+ ? collectIconRefs(iconsPath)
233
+ : { refs: new Set(), suffixes: [] };
234
+ const screenRefs = new Set();
235
+ const screensDir = resolve(projectDir, includes.screens);
236
+ for (const filePath of listFiles(screensDir, ".yaml")) {
237
+ for (const key of rootKeys(filePath)) {
238
+ screenRefs.add(`screens/${key}`);
239
+ }
240
+ }
241
+ const flowRefs = new Set();
242
+ const flowsDir = resolve(projectDir, includes.flows);
243
+ for (const filePath of listFiles(flowsDir, ".yaml")) {
244
+ for (const key of rootKeys(filePath)) {
245
+ flowRefs.add(`flows/${key}`);
246
+ }
247
+ }
248
+ const apiRefs = new Set();
249
+ if (isRecord(manifest.api) && isRecord(manifest.api.endpoints)) {
250
+ collectApiRefs("", manifest.api.endpoints, apiRefs);
251
+ }
252
+ const defaultLocale = isRecord(manifest.i18n) && typeof manifest.i18n.default_locale === "string"
253
+ ? manifest.i18n.default_locale
254
+ : localeFiles.keys().next().value ?? null;
255
+ return {
256
+ manifest,
257
+ localeFiles,
258
+ formatterNames,
259
+ mapperNames,
260
+ contractNames,
261
+ componentNames,
262
+ tokenRefs,
263
+ iconRefs: iconData.refs,
264
+ iconVariantSuffixes: iconData.suffixes,
265
+ screenRefs,
266
+ flowRefs,
267
+ apiRefs,
268
+ defaultLocale,
269
+ };
270
+ }
271
+ function missingLocalesForKey(context, key) {
272
+ const missing = [];
273
+ for (const [locale, keys] of context.localeFiles.entries()) {
274
+ if (!keys.has(key))
275
+ missing.push(locale);
276
+ }
277
+ return missing.sort();
278
+ }
279
+ function validateLocaleRef(value, path, context, errors) {
280
+ const ref = value.slice(3);
281
+ if (!ref)
282
+ return;
283
+ if (!ref.includes("{")) {
284
+ const missing = missingLocalesForKey(context, ref);
285
+ if (missing.length > 0) {
286
+ errors.push({
287
+ path,
288
+ message: `locale key "${ref}" is missing in locale(s): ${missing.join(", ")}`,
289
+ });
290
+ }
291
+ return;
292
+ }
293
+ const staticPrefix = ref.split("{", 1)[0];
294
+ if (staticPrefix) {
295
+ const hasMatch = Array.from(context.localeFiles.values()).some((keys) => Array.from(keys).some((key) => key.startsWith(staticPrefix)));
296
+ if (!hasMatch) {
297
+ errors.push({
298
+ path,
299
+ message: `dynamic locale key prefix "${staticPrefix}" does not match any locale entry`,
300
+ });
301
+ }
302
+ }
303
+ for (const formatter of extractFormatterRefs(ref)) {
304
+ if (!context.formatterNames.has(formatter)) {
305
+ errors.push({
306
+ path,
307
+ message: `unknown formatter "${formatter}" used inside locale key reference`,
308
+ });
309
+ }
310
+ }
311
+ }
312
+ function extractFormatterRefs(value) {
313
+ const refs = new Set();
314
+ const regex = /\|\s*format:([A-Za-z0-9_.-]+)/g;
315
+ let match;
316
+ while ((match = regex.exec(value)) !== null) {
317
+ const name = match[1].split(".", 1)[0];
318
+ if (name)
319
+ refs.add(name);
320
+ }
321
+ return Array.from(refs);
322
+ }
323
+ function extractMapperRefs(value) {
324
+ const refs = new Set();
325
+ const regex = /\|\s*map:([A-Za-z0-9_.-]+)/g;
326
+ let match;
327
+ while ((match = regex.exec(value)) !== null) {
328
+ if (match[1])
329
+ refs.add(match[1]);
330
+ }
331
+ return Array.from(refs);
332
+ }
333
+ function maybeTokenRef(value) {
334
+ return /^(color|typography|spacing|elevation)\./.test(value);
335
+ }
336
+ function isDynamicReference(value) {
337
+ return value.includes("{") || value.includes("}");
338
+ }
339
+ function isKnownIconRef(value, context) {
340
+ if (context.iconRefs.has(value))
341
+ return true;
342
+ for (const suffix of context.iconVariantSuffixes) {
343
+ if (value.endsWith(suffix)) {
344
+ const base = value.slice(0, -suffix.length);
345
+ if (context.iconRefs.has(base))
346
+ return true;
347
+ }
348
+ }
349
+ return false;
350
+ }
351
+ function validateStringValue(key, value, path, context, formIds, errors, options) {
352
+ if (value.startsWith("$t:")) {
353
+ validateLocaleRef(value, path, context, errors);
354
+ }
355
+ for (const formatter of extractFormatterRefs(value)) {
356
+ if (!context.formatterNames.has(formatter)) {
357
+ errors.push({ path, message: `unknown formatter "${formatter}"` });
358
+ }
359
+ }
360
+ for (const mapper of extractMapperRefs(value)) {
361
+ if (!context.mapperNames.has(mapper)) {
362
+ errors.push({ path, message: `unknown mapper "${mapper}"` });
363
+ }
364
+ }
365
+ if (options.validateTokens && maybeTokenRef(value) && !isDynamicReference(value) && !context.tokenRefs.has(value)) {
366
+ errors.push({ path, message: `unknown token reference "${value}"` });
367
+ }
368
+ if ((key === "contract" || key === "item_contract") && !context.contractNames.has(value)) {
369
+ errors.push({ path, message: `unknown contract "${value}"` });
370
+ }
371
+ if (key === "component" && !path.includes("platform_mapping") && !context.componentNames.has(value)) {
372
+ errors.push({ path, message: `unknown component "${value}"` });
373
+ }
374
+ if ((key === "icon" || key === "icon_active") &&
375
+ !isDynamicReference(value) &&
376
+ !value.includes(".") &&
377
+ !isKnownIconRef(value, context)) {
378
+ errors.push({ path, message: `unknown icon "${value}"` });
379
+ }
380
+ if ((key === "source" || key === "endpoint") && value.startsWith("api.") && !context.apiRefs.has(value)) {
381
+ errors.push({ path, message: `unknown API reference "${value}"` });
382
+ }
383
+ if (key === "destination" && value !== "$back") {
384
+ if (value.startsWith("screens/") && !context.screenRefs.has(value)) {
385
+ errors.push({ path, message: `unknown screen destination "${value}"` });
386
+ }
387
+ else if (value.startsWith("flows/") && !context.flowRefs.has(value)) {
388
+ errors.push({ path, message: `unknown flow destination "${value}"` });
389
+ }
390
+ }
391
+ if (key === "target" && value.startsWith("screens/")) {
392
+ const screenTarget = value.split(".", 1)[0];
393
+ if (!context.screenRefs.has(screenTarget)) {
394
+ errors.push({ path, message: `unknown screen target "${value}"` });
395
+ }
396
+ }
397
+ if (key === "form_id" && !formIds.has(value)) {
398
+ errors.push({ path, message: `unknown form_id "${value}" in submit action` });
399
+ }
400
+ }
401
+ function collectFormIds(value, formIds) {
402
+ if (Array.isArray(value)) {
403
+ for (const item of value)
404
+ collectFormIds(item, formIds);
405
+ return;
406
+ }
407
+ if (!isRecord(value))
408
+ return;
409
+ if (typeof value.form_id === "string" && value.form_id.trim()) {
410
+ formIds.add(value.form_id);
411
+ }
412
+ for (const nested of Object.values(value)) {
413
+ collectFormIds(nested, formIds);
414
+ }
415
+ }
416
+ function traverse(value, path, context, formIds, errors, key = null, options) {
417
+ if (Array.isArray(value)) {
418
+ for (const [index, item] of value.entries()) {
419
+ traverse(item, `${path}/${index}`, context, formIds, errors, key, options);
420
+ }
421
+ return;
422
+ }
423
+ if (typeof value === "string") {
424
+ validateStringValue(key, value, path, context, formIds, errors, options);
425
+ return;
426
+ }
427
+ if (!isRecord(value))
428
+ return;
429
+ if (key === "icon" &&
430
+ typeof value.ref === "string" &&
431
+ !isDynamicReference(value.ref) &&
432
+ !value.ref.includes(".") &&
433
+ !isKnownIconRef(value.ref, context)) {
434
+ errors.push({ path: `${path}/ref`, message: `unknown icon "${value.ref}"` });
435
+ }
436
+ if (key === "sections") {
437
+ for (const [index, entry] of Object.entries(value)) {
438
+ if (typeof entry === "string" && entry.startsWith("screens/") && !context.screenRefs.has(entry)) {
439
+ errors.push({
440
+ path: `${path}/${index}`,
441
+ message: `unknown screen section reference "${entry}"`,
442
+ });
443
+ }
444
+ }
445
+ }
446
+ for (const [childKey, nested] of Object.entries(value)) {
447
+ traverse(nested, `${path}/${childKey}`, context, formIds, errors, childKey, options);
448
+ }
449
+ }
450
+ function lintFile(filePath, context, options) {
451
+ const data = loadData(filePath);
452
+ const formIds = new Set();
453
+ collectFormIds(data, formIds);
454
+ const errors = [];
455
+ traverse(data, basename(filePath), context, formIds, errors, null, options);
456
+ return errors;
457
+ }
458
+ function lintLocaleCoverage(context) {
459
+ const errors = [];
460
+ if (context.localeFiles.size <= 1)
461
+ return errors;
462
+ const baselineLocale = (context.defaultLocale && context.localeFiles.has(context.defaultLocale) && context.defaultLocale) ||
463
+ Array.from(context.localeFiles.keys())[0];
464
+ if (!baselineLocale)
465
+ return errors;
466
+ const baselineKeys = context.localeFiles.get(baselineLocale) ?? new Set();
467
+ for (const [locale, keys] of context.localeFiles.entries()) {
468
+ if (locale === baselineLocale)
469
+ continue;
470
+ for (const key of baselineKeys) {
471
+ if (!keys.has(key)) {
472
+ errors.push({
473
+ path: `${locale}.json`,
474
+ message: `missing locale key "${key}" from baseline locale "${baselineLocale}"`,
475
+ });
476
+ }
477
+ }
478
+ }
479
+ return errors;
480
+ }
481
+ function lintManifestGenerationContext(projectDir, manifest) {
482
+ if (!isRecord(manifest))
483
+ return [];
484
+ // code_roots.backend is optional — it's a hint for AI generation, not a hard requirement
485
+ const generation = isRecord(manifest.generation) ? manifest.generation : {};
486
+ const codeRoots = isRecord(generation.code_roots) ? generation.code_roots : null;
487
+ const backendRoot = codeRoots && typeof codeRoots.backend === "string" ? codeRoots.backend.trim() : "";
488
+ if (!backendRoot)
489
+ return [];
490
+ const resolvedBackendRoot = resolve(projectDir, backendRoot);
491
+ if (!existsSync(resolvedBackendRoot)) {
492
+ return [{
493
+ path: "openuispec.yaml",
494
+ severity: "warning",
495
+ message: `generation.code_roots.backend points to a missing folder: ${backendRoot}`,
496
+ }];
497
+ }
498
+ return [];
499
+ }
500
+ function printSemanticErrors(label, errors) {
501
+ if (errors.length === 0)
502
+ return 0;
503
+ const previewLimit = 10;
504
+ const realErrors = errors.filter((e) => e.severity !== "warning");
505
+ const warnings = errors.filter((e) => e.severity === "warning");
506
+ if (realErrors.length > 0) {
507
+ console.log(` FAIL ${label} (${realErrors.length} semantic error(s))`);
508
+ for (const error of realErrors.slice(0, previewLimit)) {
509
+ console.log(` [${error.path}] ${error.message}`);
510
+ }
511
+ if (realErrors.length > previewLimit) {
512
+ console.log(` ... and ${realErrors.length - previewLimit} more`);
513
+ }
514
+ }
515
+ for (const warning of warnings) {
516
+ console.log(` WARN ${label}: ${warning.message}`);
517
+ }
518
+ return realErrors.length;
519
+ }
520
+ export function collectSemanticLint(projectDir, includes) {
521
+ const manifest = readManifest(projectDir);
522
+ const context = buildContext(projectDir, includes, manifest);
523
+ const contractsDir = resolve(projectDir, includes.contracts);
524
+ const componentsDir = resolve(projectDir, includes.components);
525
+ const allErrors = [
526
+ ...lintLocaleCoverage(context),
527
+ ...lintManifestGenerationContext(projectDir, context.manifest),
528
+ ];
529
+ const files = [
530
+ join(projectDir, "openuispec.yaml"),
531
+ ...listFiles(resolve(projectDir, includes.screens), ".yaml"),
532
+ ...listFiles(resolve(projectDir, includes.flows), ".yaml"),
533
+ ...listFiles(resolve(projectDir, includes.platform), ".yaml"),
534
+ ...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
535
+ ...listFiles(componentsDir, ".yaml"),
536
+ ];
537
+ for (const filePath of files) {
538
+ const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
539
+ allErrors.push(...lintFile(filePath, context, { validateTokens: !isContractOrComponent }));
540
+ }
541
+ return allErrors;
542
+ }
543
+ export function runSemanticLint(projectDir, includes) {
544
+ const manifest = readManifest(projectDir);
545
+ const context = buildContext(projectDir, includes, manifest);
546
+ let total = 0;
547
+ const contractsDir = resolve(projectDir, includes.contracts);
548
+ const componentsDir = resolve(projectDir, includes.components);
549
+ total += printSemanticErrors("locales", lintLocaleCoverage(context));
550
+ total += printSemanticErrors("openuispec.yaml", lintManifestGenerationContext(projectDir, context.manifest));
551
+ const files = [
552
+ join(projectDir, "openuispec.yaml"),
553
+ ...listFiles(resolve(projectDir, includes.screens), ".yaml"),
554
+ ...listFiles(resolve(projectDir, includes.flows), ".yaml"),
555
+ ...listFiles(resolve(projectDir, includes.platform), ".yaml"),
556
+ ...listFiles(resolve(projectDir, includes.contracts), ".yaml"),
557
+ ...listFiles(componentsDir, ".yaml"),
558
+ ];
559
+ for (const filePath of files) {
560
+ const isContractOrComponent = filePath.startsWith(contractsDir) || filePath.startsWith(componentsDir);
561
+ total += printSemanticErrors(basename(filePath), lintFile(filePath, context, { validateTokens: !isContractOrComponent }));
562
+ }
563
+ return total;
564
+ }